✨ 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>();
|
||||
|
||||
[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.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.");
|
||||
|
||||
var dupeNameCount = await db.Accounts.Where(a => a.Name == request.Name).CountAsync();
|
||||
if (dupeNameCount > 0)
|
||||
return BadRequest("The name is already taken.");
|
||||
|
||||
var account = new Account
|
||||
try
|
||||
{
|
||||
Name = request.Name,
|
||||
Nick = request.Nick,
|
||||
Language = request.Language,
|
||||
Contacts = new List<AccountContact>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Type = AccountContactType.Email,
|
||||
Content = request.Email,
|
||||
IsPrimary = true
|
||||
}
|
||||
},
|
||||
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;
|
||||
var account = await accounts.CreateAccount(
|
||||
request.Name,
|
||||
request.Nick,
|
||||
request.Email,
|
||||
request.Password,
|
||||
request.Language
|
||||
);
|
||||
return Ok(account);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public class RecoveryPasswordRequest
|
||||
|
@ -1,8 +1,10 @@
|
||||
using System.Globalization;
|
||||
using DysonNetwork.Sphere.Auth;
|
||||
using DysonNetwork.Sphere.Auth.OpenId;
|
||||
using DysonNetwork.Sphere.Email;
|
||||
using DysonNetwork.Sphere.Localization;
|
||||
using DysonNetwork.Sphere.Pages.Emails;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using EFCore.BulkExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -16,8 +18,9 @@ namespace DysonNetwork.Sphere.Account;
|
||||
public class AccountService(
|
||||
AppDatabase db,
|
||||
MagicSpellService spells,
|
||||
AccountUsernameService uname,
|
||||
NotificationService nty,
|
||||
EmailService email,
|
||||
EmailService mailer,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
ICacheService cache,
|
||||
ILogger<AccountService> logger
|
||||
@ -62,6 +65,114 @@ public class AccountService(
|
||||
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)
|
||||
{
|
||||
var spell = await spells.CreateMagicSpell(
|
||||
@ -265,7 +376,7 @@ public class AccountService(
|
||||
return;
|
||||
}
|
||||
|
||||
await email.SendTemplatedEmailAsync<VerificationEmail, VerificationEmailModel>(
|
||||
await mailer.SendTemplatedEmailAsync<VerificationEmail, VerificationEmailModel>(
|
||||
account.Nick,
|
||||
contact.Content,
|
||||
localizer["VerificationEmail"],
|
||||
|
@ -8,7 +8,7 @@ namespace DysonNetwork.Sphere.Account;
|
||||
/// </summary>
|
||||
public class AccountUsernameService(AppDatabase db)
|
||||
{
|
||||
private readonly Random _random = new Random();
|
||||
private readonly Random _random = new();
|
||||
|
||||
/// <summary>
|
||||
/// Generates a unique username based on the provided base name
|
||||
@ -49,31 +49,6 @@ public class AccountUsernameService(AppDatabase db)
|
||||
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>
|
||||
/// Sanitizes a username by removing invalid characters and converting to lowercase
|
||||
/// </summary>
|
||||
@ -127,26 +102,4 @@ public class AccountUsernameService(AppDatabase db)
|
||||
// Use the local part as the base for username generation
|
||||
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);
|
||||
}
|
||||
|
||||
protected override Dictionary<string, string> BuildTokenRequestParameters(string code, ProviderConfiguration config,
|
||||
string? codeVerifier)
|
||||
protected override Dictionary<string, string> BuildTokenRequestParameters(
|
||||
string code,
|
||||
ProviderConfiguration config,
|
||||
string? codeVerifier
|
||||
)
|
||||
{
|
||||
var parameters = new Dictionary<string, string>
|
||||
{
|
||||
|
@ -1,10 +1,9 @@
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using DysonNetwork.Sphere.Auth.OpenId;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth;
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
|
||||
/// <summary>
|
||||
/// This controller is designed to handle the OAuth callback.
|
||||
@ -69,43 +68,8 @@ public class AuthCallbackController(
|
||||
var account = await accounts.LookupAccount(userInfo.Email);
|
||||
if (account == null)
|
||||
{
|
||||
// Generate username and display name from email
|
||||
var username = await accountUsernameService.GenerateUsernameFromEmailAsync(userInfo.Email);
|
||||
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 new account using the AccountService
|
||||
account = await accounts.CreateAccount(userInfo);
|
||||
}
|
||||
|
||||
// Create a session for the user
|
@ -1,5 +1,8 @@
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
|
||||
@ -8,7 +11,7 @@ namespace DysonNetwork.Sphere.Auth.OpenId;
|
||||
public class OidcController(
|
||||
IServiceProvider serviceProvider,
|
||||
AppDatabase db,
|
||||
Account.AccountService accountService,
|
||||
AccountService accounts,
|
||||
AuthService authService
|
||||
)
|
||||
: 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)
|
||||
{
|
||||
return provider.ToLower() switch
|
||||
@ -45,4 +93,51 @@ public class OidcController(
|
||||
_ => 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"] ?? "",
|
||||
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 ?? "",
|
||||
AccessToken = userInfo.AccessToken,
|
||||
RefreshToken = userInfo.RefreshToken,
|
||||
LastUsedAt = NodaTime.SystemClock.Instance.GetCurrentInstant(),
|
||||
LastUsedAt = SystemClock.Instance.GetCurrentInstant(),
|
||||
AccountId = account.Id
|
||||
};
|
||||
await db.AccountConnections.AddAsync(connection);
|
||||
@ -190,6 +190,7 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
||||
ExpiredAt = now.Plus(Duration.FromHours(1)),
|
||||
StepTotal = 1,
|
||||
StepRemain = 0, // Already verified by provider
|
||||
Type = ChallengeType.Oidc,
|
||||
Platform = ChallengePlatform.Unidentified,
|
||||
Audiences = [ProviderName],
|
||||
Scopes = ["*"],
|
||||
@ -202,8 +203,8 @@ public abstract class OidcService(IConfiguration configuration, IHttpClientFacto
|
||||
var session = new Session
|
||||
{
|
||||
LastGrantedAt = now,
|
||||
Account = account,
|
||||
Challenge = challenge,
|
||||
AccountId = account.Id,
|
||||
ChallengeId = challenge.Id,
|
||||
};
|
||||
|
||||
await db.AuthSessions.AddAsync(session);
|
||||
|
@ -22,7 +22,8 @@ public class Session : ModelBase
|
||||
public enum ChallengeType
|
||||
{
|
||||
Login,
|
||||
OAuth
|
||||
OAuth, // Trying to authorize other platforms
|
||||
Oidc // Trying to connect other platforms
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -2788,6 +2846,18 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Sphere.Account.Account", "Account")
|
||||
@ -3479,6 +3549,8 @@ namespace DysonNetwork.Sphere.Migrations
|
||||
|
||||
b.Navigation("Challenges");
|
||||
|
||||
b.Navigation("Connections");
|
||||
|
||||
b.Navigation("Contacts");
|
||||
|
||||
b.Navigation("IncomingRelationships");
|
||||
|
@ -87,15 +87,13 @@
|
||||
"Oidc": {
|
||||
"Google": {
|
||||
"ClientId": "YOUR_GOOGLE_CLIENT_ID",
|
||||
"ClientSecret": "YOUR_GOOGLE_CLIENT_SECRET",
|
||||
"RedirectUri": "https://your-app.com/auth/callback/google"
|
||||
"ClientSecret": "YOUR_GOOGLE_CLIENT_SECRET"
|
||||
},
|
||||
"Apple": {
|
||||
"ClientId": "YOUR_APPLE_CLIENT_ID",
|
||||
"TeamId": "YOUR_APPLE_TEAM_ID",
|
||||
"KeyId": "YOUR_APPLE_KEY_ID",
|
||||
"PrivateKeyPath": "./apple_auth_key.p8",
|
||||
"RedirectUri": "https://your-app.com/auth/callback/apple"
|
||||
"ClientId": "dev.solsynth.solian",
|
||||
"TeamId": "W7HPZ53V6B",
|
||||
"KeyId": "B668YP4KBG",
|
||||
"PrivateKeyPath": "./Keys/Solarpass.p8"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user