using DysonNetwork.Sphere.Account; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; namespace DysonNetwork.Sphere.Auth.OpenId; /// /// This controller is designed to handle the OAuth callback. /// [ApiController] [Route("/auth/callback")] public class AuthCallbackController( AppDatabase db, AccountService accounts, MagicSpellService spells, AuthService auth, IServiceProvider serviceProvider, Account.AccountUsernameService accountUsernameService ) : ControllerBase { [HttpPost("apple")] public async Task AppleCallbackPost( [FromForm] string code, [FromForm(Name = "id_token")] string idToken, [FromForm] string? state = null, [FromForm] string? user = null) { return await ProcessOidcCallback("apple", new OidcCallbackData { Code = code, IdToken = idToken, State = state, RawData = user }); } private async Task ProcessOidcCallback(string provider, OidcCallbackData callbackData) { try { // Get the appropriate provider service var oidcService = GetOidcService(provider); // Process the callback var userInfo = await oidcService.ProcessCallbackAsync(callbackData); if (string.IsNullOrEmpty(userInfo.Email) || string.IsNullOrEmpty(userInfo.UserId)) { return BadRequest($"Email or user ID is missing from {provider}'s response"); } // First, check if we already have a connection with this provider ID var existingConnection = await db.AccountConnections .Include(c => c.Account) .FirstOrDefaultAsync(c => c.Provider == provider && c.ProvidedIdentifier == userInfo.UserId); if (existingConnection is not null) return await CreateSessionAndRedirect( oidcService, userInfo, existingConnection.Account, callbackData.State ); // If no existing connection, try to find an account by email var account = await accounts.LookupAccount(userInfo.Email); if (account == null) { // Create a new account using the AccountService account = await accounts.CreateAccount(userInfo); } // Create a session for the user var session = await oidcService.CreateSessionForUserAsync(userInfo, account); // Generate token var token = auth.CreateToken(session); // Determine where to redirect var redirectUrl = "/"; if (!string.IsNullOrEmpty(callbackData.State)) { // Use state as redirect URL (should be validated in production) redirectUrl = callbackData.State; } // Set the token as a cookie Response.Cookies.Append(AuthConstants.TokenQueryParamName, token, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Lax, Expires = DateTimeOffset.UtcNow.AddDays(30) }); return Redirect(redirectUrl); } catch (Exception ex) { return BadRequest($"Error processing {provider} Sign In: {ex.Message}"); } } private OidcService GetOidcService(string provider) { return provider.ToLower() switch { "apple" => serviceProvider.GetRequiredService(), "google" => serviceProvider.GetRequiredService(), // Add more providers as needed _ => throw new ArgumentException($"Unsupported provider: {provider}") }; } /// /// Creates a session and redirects the user with a token /// private async Task CreateSessionAndRedirect(OidcService oidcService, OidcUserInfo userInfo, Account.Account account, string? state) { // Create a session for the user var session = await oidcService.CreateSessionForUserAsync(userInfo, account); // Generate token var token = auth.CreateToken(session); // Determine where to redirect var redirectUrl = "/"; if (!string.IsNullOrEmpty(state)) redirectUrl = state; // Set the token as a cookie Response.Cookies.Append(AuthConstants.TokenQueryParamName, token, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Lax, Expires = DateTimeOffset.UtcNow.AddDays(30) }); return Redirect(redirectUrl); } }