using FlatRender.IdentitySvc.Application.Services; using FlatRender.IdentitySvc.Application.Services.Interfaces; using FlatRender.IdentitySvc.Models.Requests; using FlatRender.IdentitySvc.Models.Responses; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace FlatRender.IdentitySvc.Controllers; [ApiController] [Route("v1/auth")] public class AuthController(IAuthService authService, OAuthService oauthService) : ControllerBase { [HttpPost("register")] [AllowAnonymous] [ProducesResponseType(typeof(RegisterResponse), 201)] public async Task Register([FromBody] RegisterRequest request) { var result = await authService.RegisterAsync(request, GetClientIp()); return StatusCode(201, result); } // ── Google OAuth ──────────────────────────────────────────────────────────── [HttpGet("google/start")] [AllowAnonymous] public async Task GoogleStart([FromQuery] string return_url) { try { var url = await oauthService.BuildGoogleAuthUrlAsync(return_url ?? ""); return Redirect(url); } catch (InvalidOperationException ex) { return BadRequest(new { error = new { message = ex.Message } }); } } [HttpGet("google/callback")] [AllowAnonymous] public async Task GoogleCallback([FromQuery] string? code, [FromQuery] string? state, [FromQuery] string? error) { if (!string.IsNullOrEmpty(error) || string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state)) return BadRequest(new { error = new { message = error ?? "Missing authorization code" } }); try { var result = await oauthService.HandleGoogleCallbackAsync(code, state, GetClientIp()); // Hand tokens back to the frontend via URL fragment (not sent to servers/logs). var sep = result.ReturnUrl.Contains('#') ? "&" : "#"; var redirect = $"{result.ReturnUrl}{sep}access_token={Uri.EscapeDataString(result.Tokens.AccessToken)}" + $"&refresh_token={Uri.EscapeDataString(result.Tokens.RefreshToken)}&expires_in={result.Tokens.ExpiresIn}"; return Redirect(redirect); } catch (Exception ex) { return BadRequest(new { error = new { message = ex.Message } }); } } [HttpPost("login")] [AllowAnonymous] [ProducesResponseType(typeof(AuthTokensResponse), 200)] [ProducesResponseType(401)] [ProducesResponseType(403)] public async Task Login([FromBody] LoginRequest request) { try { var result = await authService.LoginAsync(request, GetClientIp()); return Ok(result); } catch (MfaRequiredException mfaEx) { return StatusCode(403, new { mfa_required = true, mfa_token = mfaEx.MfaToken }); } } [HttpPost("refresh")] [AllowAnonymous] [ProducesResponseType(typeof(AuthTokensResponse), 200)] public async Task Refresh([FromBody] RefreshTokenRequest request) { var result = await authService.RefreshAsync(request.RefreshToken); return Ok(result); } [HttpPost("logout")] [Authorize] [ProducesResponseType(204)] public async Task Logout() { await authService.LogoutAsync(GetSessionId(), GetUserId()); return NoContent(); } [HttpGet("sessions")] [Authorize] [ProducesResponseType(typeof(object), 200)] public async Task GetSessions() { var sessions = await authService.GetSessionsAsync(GetUserId()); return Ok(new { sessions }); } [HttpDelete("sessions/{sessionId:guid}")] [Authorize] [ProducesResponseType(204)] public async Task RevokeSession(Guid sessionId) { await authService.RevokeSessionAsync(sessionId, GetUserId()); return NoContent(); } [HttpPost("verify/email")] [AllowAnonymous] public async Task VerifyEmail([FromBody] VerifyOtpRequest request) { var ok = await authService.VerifyEmailAsync(request.Token, request.Code); return ok ? Ok() : BadRequest(new { error = "Invalid code" }); } [HttpPost("verify/phone")] [AllowAnonymous] public async Task VerifyPhone([FromBody] VerifyOtpRequest request) { var ok = await authService.VerifyPhoneAsync(request.Token, request.Code); return ok ? Ok() : BadRequest(new { error = "Invalid code" }); } [HttpPost("password/reset/request")] [AllowAnonymous] [ProducesResponseType(202)] public async Task PasswordResetRequest([FromBody] PasswordResetRequestDto request) { await authService.RequestPasswordResetAsync(request.TenantSlug, request.Email, request.PhoneNumber); return Accepted(); } [HttpPost("password/reset/confirm")] [AllowAnonymous] public async Task PasswordResetConfirm([FromBody] PasswordResetConfirmRequest request) { await authService.ConfirmPasswordResetAsync(request.Token, request.NewPassword); return Ok(); } [HttpPost("password/change")] [Authorize] [ProducesResponseType(204)] public async Task PasswordChange([FromBody] PasswordChangeRequest request) { await authService.ChangePasswordAsync(GetUserId(), request.CurrentPassword, request.NewPassword); return NoContent(); } [HttpPost("mfa/setup")] [Authorize] [ProducesResponseType(typeof(MfaSetupResponse), 200)] public async Task MfaSetup([FromBody] MfaSetupRequest request) { var result = await authService.SetupMfaAsync(GetUserId(), request.FactorType, request.Label); return Ok(result); } [HttpPost("mfa/verify")] [Authorize] public async Task MfaVerify([FromBody] MfaVerifyRequest request) { var ok = await authService.VerifyMfaAsync(GetUserId(), request.FactorId, request.Code); return ok ? Ok() : BadRequest(new { error = "Invalid code" }); } [HttpPost("mfa/challenge")] [AllowAnonymous] [ProducesResponseType(typeof(AuthTokensResponse), 200)] public async Task MfaChallenge([FromBody] MfaChallengeRequest request) { var result = await authService.ChallengeMfaAsync(request.MfaToken, request.Code); return Ok(result); } [HttpPost("push/subscribe")] [Authorize] [ProducesResponseType(201)] public async Task PushSubscribe([FromBody] PushSubscribeRequest request) { await authService.SubscribePushAsync( GetUserId(), GetTenantId(), request.Endpoint, request.Keys.P256dh, request.Keys.Auth, request.UserAgent); return StatusCode(201); } [HttpPost("push/unsubscribe")] [Authorize] [ProducesResponseType(204)] public async Task PushUnsubscribe([FromBody] PushUnsubscribeRequest request) { await authService.UnsubscribePushAsync(GetUserId(), request.Endpoint); return NoContent(); } // ── Helpers ─────────────────────────────────────────────────────────── private Guid GetUserId() => Guid.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? User.FindFirst("sub")?.Value ?? throw new UnauthorizedAccessException()); private Guid GetTenantId() => Guid.Parse(User.FindFirst("tenant_id")?.Value ?? throw new UnauthorizedAccessException()); private Guid GetSessionId() => Guid.Parse(User.FindFirst("jti")?.Value ?? Guid.Empty.ToString()); private string? GetClientIp() => HttpContext.Connection.RemoteIpAddress?.ToString(); }