✨ Login with Apple
This commit is contained in:
parent
bf013a108b
commit
16ff5588b9
@ -25,6 +25,7 @@ public class Account : ModelBase
|
|||||||
public ICollection<Badge> Badges { get; set; } = new List<Badge>();
|
public ICollection<Badge> Badges { get; set; } = new List<Badge>();
|
||||||
|
|
||||||
[JsonIgnore] public ICollection<AccountAuthFactor> AuthFactors { get; set; } = new List<AccountAuthFactor>();
|
[JsonIgnore] public ICollection<AccountAuthFactor> AuthFactors { get; set; } = new List<AccountAuthFactor>();
|
||||||
|
[JsonIgnore] public ICollection<AccountConnection> Connections { get; set; } = new List<AccountConnection>();
|
||||||
[JsonIgnore] public ICollection<Auth.Session> Sessions { get; set; } = new List<Auth.Session>();
|
[JsonIgnore] public ICollection<Auth.Session> Sessions { get; set; } = new List<Auth.Session>();
|
||||||
[JsonIgnore] public ICollection<Auth.Challenge> Challenges { get; set; } = new List<Auth.Challenge>();
|
[JsonIgnore] public ICollection<Auth.Challenge> Challenges { get; set; } = new List<Auth.Challenge>();
|
||||||
|
|
||||||
|
@ -82,51 +82,21 @@ public class AccountController(
|
|||||||
{
|
{
|
||||||
if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token.");
|
if (!await auth.ValidateCaptcha(request.CaptchaToken)) return BadRequest("Invalid captcha token.");
|
||||||
|
|
||||||
var dupeNameCount = await db.Accounts.Where(a => a.Name == request.Name).CountAsync();
|
try
|
||||||
if (dupeNameCount > 0)
|
|
||||||
return BadRequest("The name is already taken.");
|
|
||||||
|
|
||||||
var account = new Account
|
|
||||||
{
|
{
|
||||||
Name = request.Name,
|
var account = await accounts.CreateAccount(
|
||||||
Nick = request.Nick,
|
request.Name,
|
||||||
Language = request.Language,
|
request.Nick,
|
||||||
Contacts = new List<AccountContact>
|
request.Email,
|
||||||
{
|
request.Password,
|
||||||
new()
|
request.Language
|
||||||
{
|
);
|
||||||
Type = AccountContactType.Email,
|
return Ok(account);
|
||||||
Content = request.Email,
|
}
|
||||||
IsPrimary = true
|
catch (Exception ex)
|
||||||
}
|
{
|
||||||
},
|
return BadRequest(ex.Message);
|
||||||
AuthFactors = new List<AccountAuthFactor>
|
}
|
||||||
{
|
|
||||||
new AccountAuthFactor
|
|
||||||
{
|
|
||||||
Type = AccountAuthFactorType.Password,
|
|
||||||
Secret = request.Password,
|
|
||||||
EnabledAt = SystemClock.Instance.GetCurrentInstant()
|
|
||||||
}.HashSecret()
|
|
||||||
},
|
|
||||||
Profile = new Profile()
|
|
||||||
};
|
|
||||||
|
|
||||||
await db.Accounts.AddAsync(account);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
var spell = await spells.CreateMagicSpell(
|
|
||||||
account,
|
|
||||||
MagicSpellType.AccountActivation,
|
|
||||||
new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
{ "contact_method", account.Contacts.First().Content }
|
|
||||||
},
|
|
||||||
expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(7))
|
|
||||||
);
|
|
||||||
await spells.NotifyMagicSpell(spell, true);
|
|
||||||
|
|
||||||
return account;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RecoveryPasswordRequest
|
public class RecoveryPasswordRequest
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using DysonNetwork.Sphere.Auth;
|
using DysonNetwork.Sphere.Auth;
|
||||||
|
using DysonNetwork.Sphere.Auth.OpenId;
|
||||||
using DysonNetwork.Sphere.Email;
|
using DysonNetwork.Sphere.Email;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Pages.Emails;
|
using DysonNetwork.Sphere.Pages.Emails;
|
||||||
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using EFCore.BulkExtensions;
|
using EFCore.BulkExtensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -16,8 +18,9 @@ namespace DysonNetwork.Sphere.Account;
|
|||||||
public class AccountService(
|
public class AccountService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
MagicSpellService spells,
|
MagicSpellService spells,
|
||||||
|
AccountUsernameService uname,
|
||||||
NotificationService nty,
|
NotificationService nty,
|
||||||
EmailService email,
|
EmailService mailer,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
ILogger<AccountService> logger
|
ILogger<AccountService> logger
|
||||||
@ -62,6 +65,114 @@ public class AccountService(
|
|||||||
return profile?.Level;
|
return profile?.Level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Account> CreateAccount(
|
||||||
|
string name,
|
||||||
|
string nick,
|
||||||
|
string email,
|
||||||
|
string? password,
|
||||||
|
string language = "en-US",
|
||||||
|
bool isEmailVerified = false,
|
||||||
|
bool isActivated = false
|
||||||
|
)
|
||||||
|
{
|
||||||
|
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dupeNameCount = await db.Accounts.Where(a => a.Name == name).CountAsync();
|
||||||
|
if (dupeNameCount > 0)
|
||||||
|
throw new InvalidOperationException("Account name has already been taken.");
|
||||||
|
|
||||||
|
var account = new Account
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Nick = nick,
|
||||||
|
Language = language,
|
||||||
|
Contacts = new List<AccountContact>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Type = AccountContactType.Email,
|
||||||
|
Content = email,
|
||||||
|
VerifiedAt = isEmailVerified ? SystemClock.Instance.GetCurrentInstant() : null,
|
||||||
|
IsPrimary = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AuthFactors = password is not null
|
||||||
|
? new List<AccountAuthFactor>
|
||||||
|
{
|
||||||
|
new AccountAuthFactor
|
||||||
|
{
|
||||||
|
Type = AccountAuthFactorType.Password,
|
||||||
|
Secret = password,
|
||||||
|
EnabledAt = SystemClock.Instance.GetCurrentInstant()
|
||||||
|
}.HashSecret()
|
||||||
|
}
|
||||||
|
: [],
|
||||||
|
Profile = new Profile()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isActivated)
|
||||||
|
{
|
||||||
|
account.ActivatedAt = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
var defaultGroup = await db.PermissionGroups.FirstOrDefaultAsync(g => g.Key == "default");
|
||||||
|
if (defaultGroup is not null)
|
||||||
|
{
|
||||||
|
db.PermissionGroupMembers.Add(new PermissionGroupMember
|
||||||
|
{
|
||||||
|
Actor = $"user:{account.Id}",
|
||||||
|
Group = defaultGroup
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var spell = await spells.CreateMagicSpell(
|
||||||
|
account,
|
||||||
|
MagicSpellType.AccountActivation,
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "contact_method", account.Contacts.First().Content }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await spells.NotifyMagicSpell(spell, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Accounts.Add(account);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Account> CreateAccount(OidcUserInfo userInfo)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(userInfo.Email))
|
||||||
|
throw new ArgumentException("Email is required for account creation");
|
||||||
|
|
||||||
|
var displayName = !string.IsNullOrEmpty(userInfo.DisplayName)
|
||||||
|
? userInfo.DisplayName
|
||||||
|
: $"{userInfo.FirstName} {userInfo.LastName}".Trim();
|
||||||
|
|
||||||
|
// Generate username from email
|
||||||
|
var username = await uname.GenerateUsernameFromEmailAsync(userInfo.Email);
|
||||||
|
|
||||||
|
return await CreateAccount(
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
userInfo.Email,
|
||||||
|
null,
|
||||||
|
"en-US",
|
||||||
|
userInfo.EmailVerified,
|
||||||
|
userInfo.EmailVerified
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RequestAccountDeletion(Account account)
|
public async Task RequestAccountDeletion(Account account)
|
||||||
{
|
{
|
||||||
var spell = await spells.CreateMagicSpell(
|
var spell = await spells.CreateMagicSpell(
|
||||||
@ -265,7 +376,7 @@ public class AccountService(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await email.SendTemplatedEmailAsync<VerificationEmail, VerificationEmailModel>(
|
await mailer.SendTemplatedEmailAsync<VerificationEmail, VerificationEmailModel>(
|
||||||
account.Nick,
|
account.Nick,
|
||||||
contact.Content,
|
contact.Content,
|
||||||
localizer["VerificationEmail"],
|
localizer["VerificationEmail"],
|
||||||
|
@ -8,7 +8,7 @@ namespace DysonNetwork.Sphere.Account;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class AccountUsernameService(AppDatabase db)
|
public class AccountUsernameService(AppDatabase db)
|
||||||
{
|
{
|
||||||
private readonly Random _random = new Random();
|
private readonly Random _random = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a unique username based on the provided base name
|
/// Generates a unique username based on the provided base name
|
||||||
@ -49,31 +49,6 @@ public class AccountUsernameService(AppDatabase db)
|
|||||||
return $"{sanitized}{timestamp}";
|
return $"{sanitized}{timestamp}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generates a display name, adding numbers if needed
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="baseName">The preferred display name</param>
|
|
||||||
/// <returns>A display name with optional suffix</returns>
|
|
||||||
public Task<string> GenerateUniqueDisplayNameAsync(string baseName)
|
|
||||||
{
|
|
||||||
// If the base name is empty, use a default
|
|
||||||
if (string.IsNullOrEmpty(baseName))
|
|
||||||
{
|
|
||||||
baseName = "User";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate if too long
|
|
||||||
if (baseName.Length > 50)
|
|
||||||
{
|
|
||||||
baseName = baseName.Substring(0, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since display names can be duplicated, just return the base name
|
|
||||||
// But add a random suffix to make it more unique visually
|
|
||||||
var suffix = _random.Next(1000, 9999);
|
|
||||||
return Task.FromResult($"{baseName}{suffix}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sanitizes a username by removing invalid characters and converting to lowercase
|
/// Sanitizes a username by removing invalid characters and converting to lowercase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -127,26 +102,4 @@ public class AccountUsernameService(AppDatabase db)
|
|||||||
// Use the local part as the base for username generation
|
// Use the local part as the base for username generation
|
||||||
return await GenerateUniqueUsernameAsync(localPart);
|
return await GenerateUniqueUsernameAsync(localPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generates a display name from an email address
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="email">The email address to generate a display name from</param>
|
|
||||||
/// <returns>A display name derived from the email</returns>
|
|
||||||
public async Task<string> GenerateDisplayNameFromEmailAsync(string email)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(email))
|
|
||||||
return await GenerateUniqueDisplayNameAsync("User");
|
|
||||||
|
|
||||||
// Extract the local part of the email (before the @)
|
|
||||||
var localPart = email.Split('@')[0];
|
|
||||||
|
|
||||||
// Capitalize first letter and replace dots/underscores with spaces
|
|
||||||
var displayName = Regex.Replace(localPart, @"[._-]+", " ");
|
|
||||||
|
|
||||||
// Capitalize words
|
|
||||||
displayName = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(displayName);
|
|
||||||
|
|
||||||
return await GenerateUniqueDisplayNameAsync(displayName);
|
|
||||||
}
|
|
||||||
}
|
}
|
13
DysonNetwork.Sphere/Auth/OpenId/AppleMobileSignInRequest.cs
Normal file
13
DysonNetwork.Sphere/Auth/OpenId/AppleMobileSignInRequest.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
|
public class AppleMobileSignInRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public required string IdentityToken { get; set; }
|
||||||
|
[Required]
|
||||||
|
public required string AuthorizationCode { get; set; }
|
||||||
|
}
|
@ -102,8 +102,11 @@ public class AppleOidcService(
|
|||||||
return ValidateAndExtractIdToken(idToken, validationParameters);
|
return ValidateAndExtractIdToken(idToken, validationParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Dictionary<string, string> BuildTokenRequestParameters(string code, ProviderConfiguration config,
|
protected override Dictionary<string, string> BuildTokenRequestParameters(
|
||||||
string? codeVerifier)
|
string code,
|
||||||
|
ProviderConfiguration config,
|
||||||
|
string? codeVerifier
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var parameters = new Dictionary<string, string>
|
var parameters = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using DysonNetwork.Sphere.Auth.OpenId;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This controller is designed to handle the OAuth callback.
|
/// This controller is designed to handle the OAuth callback.
|
||||||
@ -69,43 +68,8 @@ public class AuthCallbackController(
|
|||||||
var account = await accounts.LookupAccount(userInfo.Email);
|
var account = await accounts.LookupAccount(userInfo.Email);
|
||||||
if (account == null)
|
if (account == null)
|
||||||
{
|
{
|
||||||
// Generate username and display name from email
|
// Create a new account using the AccountService
|
||||||
var username = await accountUsernameService.GenerateUsernameFromEmailAsync(userInfo.Email);
|
account = await accounts.CreateAccount(userInfo);
|
||||||
var displayName = await accountUsernameService.GenerateDisplayNameFromEmailAsync(userInfo.Email);
|
|
||||||
|
|
||||||
// Create a new account
|
|
||||||
account = new Account.Account
|
|
||||||
{
|
|
||||||
Name = username,
|
|
||||||
Nick = displayName,
|
|
||||||
Contacts = new List<AccountContact>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Type = AccountContactType.Email,
|
|
||||||
Content = userInfo.Email,
|
|
||||||
VerifiedAt = userInfo.EmailVerified ? SystemClock.Instance.GetCurrentInstant() : null,
|
|
||||||
IsPrimary = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Profile = new Profile()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save the account
|
|
||||||
await db.Accounts.AddAsync(account);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
// Do the usual steps
|
|
||||||
var spell = await spells.CreateMagicSpell(
|
|
||||||
account,
|
|
||||||
MagicSpellType.AccountActivation,
|
|
||||||
new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
{ "contact_method", account.Contacts.First().Content }
|
|
||||||
},
|
|
||||||
expiredAt: SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(7))
|
|
||||||
);
|
|
||||||
await spells.NotifyMagicSpell(spell, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a session for the user
|
// Create a session for the user
|
@ -1,5 +1,8 @@
|
|||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||||
|
|
||||||
@ -8,7 +11,7 @@ namespace DysonNetwork.Sphere.Auth.OpenId;
|
|||||||
public class OidcController(
|
public class OidcController(
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
Account.AccountService accountService,
|
AccountService accounts,
|
||||||
AuthService authService
|
AuthService authService
|
||||||
)
|
)
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
@ -35,6 +38,51 @@ public class OidcController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mobile Apple Sign In endpoint
|
||||||
|
/// Handles Apple authentication directly from mobile apps
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("apple/mobile")]
|
||||||
|
public async Task<ActionResult<AuthController.TokenExchangeResponse>> AppleMobileSignIn([FromBody] AppleMobileSignInRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get Apple OIDC service
|
||||||
|
if (GetOidcService("apple") is not AppleOidcService appleService)
|
||||||
|
return StatusCode(503, "Apple OIDC service not available");
|
||||||
|
|
||||||
|
// Prepare callback data for processing
|
||||||
|
var callbackData = new OidcCallbackData
|
||||||
|
{
|
||||||
|
IdToken = request.IdentityToken,
|
||||||
|
Code = request.AuthorizationCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process the authentication
|
||||||
|
var userInfo = await appleService.ProcessCallbackAsync(callbackData);
|
||||||
|
|
||||||
|
// Find or create user account using existing logic
|
||||||
|
var account = await FindOrCreateAccount(userInfo, "apple");
|
||||||
|
|
||||||
|
// Create session using the OIDC service
|
||||||
|
var session = await appleService.CreateSessionForUserAsync(userInfo, account);
|
||||||
|
|
||||||
|
// Generate token using existing auth service
|
||||||
|
var token = authService.CreateToken(session);
|
||||||
|
|
||||||
|
return Ok(new AuthController.TokenExchangeResponse { Token = token });
|
||||||
|
}
|
||||||
|
catch (SecurityTokenValidationException ex)
|
||||||
|
{
|
||||||
|
return Unauthorized($"Invalid identity token: {ex.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Log the error
|
||||||
|
return StatusCode(500, $"Authentication failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private OidcService GetOidcService(string provider)
|
private OidcService GetOidcService(string provider)
|
||||||
{
|
{
|
||||||
return provider.ToLower() switch
|
return provider.ToLower() switch
|
||||||
@ -45,4 +93,51 @@ public class OidcController(
|
|||||||
_ => throw new ArgumentException($"Unsupported provider: {provider}")
|
_ => throw new ArgumentException($"Unsupported provider: {provider}")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Account.Account> FindOrCreateAccount(OidcUserInfo userInfo, string provider)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(userInfo.Email))
|
||||||
|
throw new ArgumentException("Email is required for account creation");
|
||||||
|
|
||||||
|
// Check if account exists by email
|
||||||
|
var existingAccount = await accounts.LookupAccount(userInfo.Email);
|
||||||
|
if (existingAccount != null)
|
||||||
|
{
|
||||||
|
// Check if this provider connection already exists
|
||||||
|
var existingConnection = await db.AccountConnections
|
||||||
|
.FirstOrDefaultAsync(c => c.AccountId == existingAccount.Id &&
|
||||||
|
c.Provider == provider &&
|
||||||
|
c.ProvidedIdentifier == userInfo.UserId);
|
||||||
|
|
||||||
|
// If no connection exists, create one
|
||||||
|
if (existingConnection != null) return existingAccount;
|
||||||
|
var connection = new AccountConnection
|
||||||
|
{
|
||||||
|
AccountId = existingAccount.Id,
|
||||||
|
Provider = provider,
|
||||||
|
ProvidedIdentifier = userInfo.UserId!,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.AccountConnections.Add(connection);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return existingAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new account using the AccountService
|
||||||
|
var newAccount = await accounts.CreateAccount(userInfo);
|
||||||
|
|
||||||
|
// Create the provider connection
|
||||||
|
var newConnection = new AccountConnection
|
||||||
|
{
|
||||||
|
AccountId = newAccount.Id,
|
||||||
|
Provider = provider,
|
||||||
|
ProvidedIdentifier = userInfo.UserId!,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.AccountConnections.Add(newConnection);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return newAccount;
|
||||||
|
}
|
||||||
}
|
}
|
@ -49,7 +49,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
|||||||
{
|
{
|
||||||
ClientId = configuration[$"Oidc:{ConfigSectionName}:ClientId"] ?? "",
|
ClientId = configuration[$"Oidc:{ConfigSectionName}:ClientId"] ?? "",
|
||||||
ClientSecret = configuration[$"Oidc:{ConfigSectionName}:ClientSecret"] ?? "",
|
ClientSecret = configuration[$"Oidc:{ConfigSectionName}:ClientSecret"] ?? "",
|
||||||
RedirectUri = configuration[$"Oidc:{ConfigSectionName}:RedirectUri"] ?? ""
|
RedirectUri = configuration["BaseUrl"] + "/auth/callback/" + ProviderName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
|||||||
ProvidedIdentifier = userInfo.UserId ?? "",
|
ProvidedIdentifier = userInfo.UserId ?? "",
|
||||||
AccessToken = userInfo.AccessToken,
|
AccessToken = userInfo.AccessToken,
|
||||||
RefreshToken = userInfo.RefreshToken,
|
RefreshToken = userInfo.RefreshToken,
|
||||||
LastUsedAt = NodaTime.SystemClock.Instance.GetCurrentInstant(),
|
LastUsedAt = SystemClock.Instance.GetCurrentInstant(),
|
||||||
AccountId = account.Id
|
AccountId = account.Id
|
||||||
};
|
};
|
||||||
await db.AccountConnections.AddAsync(connection);
|
await db.AccountConnections.AddAsync(connection);
|
||||||
@ -190,6 +190,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
|||||||
ExpiredAt = now.Plus(Duration.FromHours(1)),
|
ExpiredAt = now.Plus(Duration.FromHours(1)),
|
||||||
StepTotal = 1,
|
StepTotal = 1,
|
||||||
StepRemain = 0, // Already verified by provider
|
StepRemain = 0, // Already verified by provider
|
||||||
|
Type = ChallengeType.Oidc,
|
||||||
Platform = ChallengePlatform.Unidentified,
|
Platform = ChallengePlatform.Unidentified,
|
||||||
Audiences = [ProviderName],
|
Audiences = [ProviderName],
|
||||||
Scopes = ["*"],
|
Scopes = ["*"],
|
||||||
@ -202,8 +203,8 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
|||||||
var session = new Session
|
var session = new Session
|
||||||
{
|
{
|
||||||
LastGrantedAt = now,
|
LastGrantedAt = now,
|
||||||
Account = account,
|
AccountId = account.Id,
|
||||||
Challenge = challenge,
|
ChallengeId = challenge.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.AuthSessions.AddAsync(session);
|
await db.AuthSessions.AddAsync(session);
|
||||||
|
@ -22,7 +22,8 @@ public class Session : ModelBase
|
|||||||
public enum ChallengeType
|
public enum ChallengeType
|
||||||
{
|
{
|
||||||
Login,
|
Login,
|
||||||
OAuth
|
OAuth, // Trying to authorize other platforms
|
||||||
|
Oidc // Trying to connect other platforms
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ChallengePlatform
|
public enum ChallengePlatform
|
||||||
|
3622
DysonNetwork.Sphere/Migrations/20250615083256_AddAccountConnection.Designer.cs
generated
Normal file
3622
DysonNetwork.Sphere/Migrations/20250615083256_AddAccountConnection.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAccountConnection : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "account_connections",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
provider = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||||
|
provided_identifier = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
|
||||||
|
access_token = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||||
|
refresh_token = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||||
|
last_used_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||||
|
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||||
|
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_account_connections", x => x.id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "fk_account_connections_accounts_account_id",
|
||||||
|
column: x => x.account_id,
|
||||||
|
principalTable: "accounts",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_account_connections_account_id",
|
||||||
|
table: "account_connections",
|
||||||
|
column: "account_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "account_connections");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -144,6 +144,64 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.ToTable("account_auth_factors", (string)null);
|
b.ToTable("account_auth_factors", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountConnection", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<string>("AccessToken")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("access_token");
|
||||||
|
|
||||||
|
b.Property<Guid>("AccountId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("account_id");
|
||||||
|
|
||||||
|
b.Property<Instant>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<Instant?>("LastUsedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("last_used_at");
|
||||||
|
|
||||||
|
b.Property<string>("ProvidedIdentifier")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(8192)
|
||||||
|
.HasColumnType("character varying(8192)")
|
||||||
|
.HasColumnName("provided_identifier");
|
||||||
|
|
||||||
|
b.Property<string>("Provider")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("provider");
|
||||||
|
|
||||||
|
b.Property<string>("RefreshToken")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("refresh_token");
|
||||||
|
|
||||||
|
b.Property<Instant>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated_at");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_account_connections");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId")
|
||||||
|
.HasDatabaseName("ix_account_connections_account_id");
|
||||||
|
|
||||||
|
b.ToTable("account_connections", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -2788,6 +2846,18 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
b.Navigation("Account");
|
b.Navigation("Account");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountConnection", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
||||||
|
.WithMany("Connections")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired()
|
||||||
|
.HasConstraintName("fk_account_connections_accounts_account_id");
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b =>
|
modelBuilder.Entity("DysonNetwork.Sphere.Account.AccountContact", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
||||||
@ -3479,6 +3549,8 @@ namespace DysonNetwork.Sphere.Migrations
|
|||||||
|
|
||||||
b.Navigation("Challenges");
|
b.Navigation("Challenges");
|
||||||
|
|
||||||
|
b.Navigation("Connections");
|
||||||
|
|
||||||
b.Navigation("Contacts");
|
b.Navigation("Contacts");
|
||||||
|
|
||||||
b.Navigation("IncomingRelationships");
|
b.Navigation("IncomingRelationships");
|
||||||
|
@ -87,15 +87,13 @@
|
|||||||
"Oidc": {
|
"Oidc": {
|
||||||
"Google": {
|
"Google": {
|
||||||
"ClientId": "YOUR_GOOGLE_CLIENT_ID",
|
"ClientId": "YOUR_GOOGLE_CLIENT_ID",
|
||||||
"ClientSecret": "YOUR_GOOGLE_CLIENT_SECRET",
|
"ClientSecret": "YOUR_GOOGLE_CLIENT_SECRET"
|
||||||
"RedirectUri": "https://your-app.com/auth/callback/google"
|
|
||||||
},
|
},
|
||||||
"Apple": {
|
"Apple": {
|
||||||
"ClientId": "YOUR_APPLE_CLIENT_ID",
|
"ClientId": "dev.solsynth.solian",
|
||||||
"TeamId": "YOUR_APPLE_TEAM_ID",
|
"TeamId": "W7HPZ53V6B",
|
||||||
"KeyId": "YOUR_APPLE_KEY_ID",
|
"KeyId": "B668YP4KBG",
|
||||||
"PrivateKeyPath": "./apple_auth_key.p8",
|
"PrivateKeyPath": "./Keys/Solarpass.p8"
|
||||||
"RedirectUri": "https://your-app.com/auth/callback/apple"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user