using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Tiku.Application.Auth; using Tiku.Api.Contracts; using Tiku.Application.Security; using Tiku.Application.Tenancy; using Tiku.Infrastructure.Content; namespace Tiku.Api.Controllers; [ApiController] [Route("api/auth")] [Produces("application/json")] public sealed class AuthController( IAuthService authService, ISessionService sessionService, ITenantContext tenantContext, ITenantContextInitializer tenantContextInitializer, ITenantDirectory tenantDirectory) : ControllerBase { [AllowAnonymous] [HttpPost("login/password")] [EndpointSummary("手机号密码登录")] [EndpointDescription("使用本地手机号和密码登录,签发 JWT access token 与数据库 refresh/session。")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task> LoginWithPassword( [FromBody] PasswordLoginDto request, CancellationToken cancellationToken) { var result = await authService.LoginWithPasswordAsync( new PasswordLoginRequest( await ResolveTenantIdAsync(request.TenantCode, cancellationToken), request.Phone, request.Password, GetIpAddress(), Request.Headers.UserAgent.ToString()), cancellationToken); return Ok(AuthenticatedUserDto.FromApplication(result)); } [AllowAnonymous] [HttpPost("login/sms")] [EndpointSummary("短信验证码登录")] [EndpointDescription("校验已发送的登录用途短信验证码,成功后签发 JWT access token 与数据库 refresh/session。")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task> LoginWithSms( [FromBody] SmsLoginDto request, CancellationToken cancellationToken) { var result = await authService.LoginWithSmsAsync( new SmsLoginRequest( await ResolveTenantIdAsync(request.TenantCode, cancellationToken), request.Phone, request.Code, GetIpAddress(), Request.Headers.UserAgent.ToString()), cancellationToken); return Ok(AuthenticatedUserDto.FromApplication(result)); } [AllowAnonymous] [HttpPost("oauth/wechat")] [EndpointSummary("微信网页 OAuth 登录")] [EndpointDescription("使用微信网页授权 code 换取 openid/unionid,upsert 用户身份并创建应用会话。")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] public async Task> LoginWithWechatWeb( [FromBody] OAuthCodeDto request, CancellationToken cancellationToken) { var result = await authService.LoginWithWechatWebAsync( new WechatLoginRequest( await ResolveTenantIdAsync(request.TenantCode, cancellationToken), request.Code, GetIpAddress(), Request.Headers.UserAgent.ToString()), cancellationToken); return Ok(AuthenticatedUserDto.FromApplication(result)); } [AllowAnonymous] [HttpPost("oauth/wechat-miniapp")] [EndpointSummary("微信小程序登录")] [EndpointDescription("使用小程序 wx.login 返回的 code 换取 openid/session_key,upsert 用户身份并创建应用会话。")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] public async Task> LoginWithWechatMiniApp( [FromBody] OAuthCodeDto request, CancellationToken cancellationToken) { var result = await authService.LoginWithWechatMiniAppAsync( new WechatLoginRequest( await ResolveTenantIdAsync(request.TenantCode, cancellationToken), request.Code, GetIpAddress(), Request.Headers.UserAgent.ToString()), cancellationToken); return Ok(AuthenticatedUserDto.FromApplication(result)); } [AllowAnonymous] [HttpPost("refresh")] [EndpointSummary("刷新登录会话")] [EndpointDescription("使用 refresh token 轮换数据库 session,并签发新的 access/refresh token。")] public async Task> Refresh( [FromBody] RefreshSessionDto request, CancellationToken cancellationToken) { ResolveRefreshTokenTenant(request.RefreshToken); var result = await authService.RefreshAsync( new RefreshSessionRequest( request.RefreshToken, GetIpAddress(), Request.Headers.UserAgent.ToString()), cancellationToken); return Ok(result); } [AllowAnonymous] [HttpPost("logout")] [EndpointSummary("退出登录")] [EndpointDescription("撤销 refresh token 对应的数据库 session;session 校验开启时,旧 access token 也会被拒绝。")] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Logout( [FromBody] RefreshSessionDto request, CancellationToken cancellationToken) { ResolveRefreshTokenTenant(request.RefreshToken); await authService.LogoutAsync( new LogoutSessionRequest(request.RefreshToken), cancellationToken); return NoContent(); } private void ResolveRefreshTokenTenant(string refreshToken) { if (!sessionService.TryParseRefreshToken(refreshToken, out var locator)) { return; } tenantContextInitializer.Initialize(locator.TenantId, null, TenantResolutionSource.RefreshToken); } private string? GetIpAddress() { return HttpContext.Connection.RemoteIpAddress?.ToString(); } private async Task ResolveTenantIdAsync(string? tenantCode, CancellationToken cancellationToken) { if (tenantContext.TenantId.HasValue) { if (!string.IsNullOrWhiteSpace(tenantCode) && !string.Equals(tenantContext.TenantCode, tenantCode.Trim(), StringComparison.OrdinalIgnoreCase)) { var supplied = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken); if (supplied?.TenantId != tenantContext.TenantId.Value) { throw new TenantContextConflictException( tenantContext.TenantId.Value, supplied?.TenantId ?? Guid.Empty); } } return tenantContext.TenantId.Value; } if (string.IsNullOrWhiteSpace(tenantCode)) { throw new RequiredFieldException("tenantCode is required when the request host does not resolve a tenant."); } var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken) ?? throw new TenantNotFoundException(); tenantContextInitializer.Initialize( tenant.TenantId, tenant.TenantCode, TenantResolutionSource.TenantCode); return tenant.TenantId; } }