feat: add local authentication

This commit is contained in:
xiong
2026-07-26 12:51:56 +08:00
parent e8b03c57bc
commit 2b02bbef7b
21 changed files with 1014 additions and 15 deletions

View File

@@ -0,0 +1,56 @@
using Tiku.Domain.Tenancy;
namespace Tiku.Application.Auth;
public sealed record AuthTokenPair(
string AccessToken,
string RefreshToken,
DateTimeOffset AccessTokenExpiresAt,
DateTimeOffset RefreshTokenExpiresAt);
public sealed record TenantMembershipSummary(
Guid TenantId,
string TenantName,
TenantRole Role,
MembershipStatus Status);
public sealed record AuthenticatedUser(
Guid UserId,
string? Phone,
string? Email,
string? Name,
TenantMembershipSummary Tenant,
AuthTokenPair Tokens);
public sealed record PasswordLoginRequest(
Guid TenantId,
string Phone,
string Password,
string? IpAddress,
string? UserAgent);
public sealed record SmsLoginRequest(
Guid TenantId,
string Phone,
string Code,
string? IpAddress,
string? UserAgent);
public sealed record RefreshSessionRequest(
string RefreshToken,
string? IpAddress,
string? UserAgent);
public sealed record LogoutSessionRequest(
string RefreshToken);
public sealed record SmsSendResult(
Guid VerificationId,
DateTimeOffset ExpiresAt);
public sealed record SendSmsCodeRequest(
Guid TenantId,
string Phone,
SmsPurpose Purpose,
string? IpAddress,
string? UserAgent);

View File

@@ -0,0 +1,18 @@
namespace Tiku.Application.Auth;
public class AuthException(string code, string message) : Exception(message)
{
public string Code { get; } = code;
}
public sealed class InvalidCredentialsException(string code = "invalid_credentials")
: AuthException(code, "The supplied credentials are invalid.");
public sealed class TenantAccessDeniedException()
: AuthException("tenant_access_denied", "The user is not an active member of the requested tenant.");
public sealed class SessionRevokedException()
: AuthException("session_revoked", "The session has been revoked or expired.");
public sealed class SmsRateLimitedException()
: AuthException("sms_rate_limited", "SMS verification requests are rate limited.");

View File

@@ -0,0 +1,20 @@
namespace Tiku.Application.Auth;
public interface IAuthService
{
Task<AuthenticatedUser> LoginWithPasswordAsync(
PasswordLoginRequest request,
CancellationToken cancellationToken = default);
Task<AuthenticatedUser> LoginWithSmsAsync(
SmsLoginRequest request,
CancellationToken cancellationToken = default);
Task<AuthTokenPair> RefreshAsync(
RefreshSessionRequest request,
CancellationToken cancellationToken = default);
Task LogoutAsync(
LogoutSessionRequest request,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,7 @@
namespace Tiku.Application.Auth;
public interface IPasswordHasher
{
string Hash(string password);
bool Verify(string password, string passwordHash);
}

View File

@@ -0,0 +1,19 @@
using Tiku.Domain.Tenancy;
namespace Tiku.Application.Auth;
public interface ISessionService
{
string GenerateRefreshToken();
string HashRefreshToken(string refreshToken);
Task<AuthTokenPair> IssueAsync(
Guid userId,
string? phone,
string? email,
TenantMembership membership,
string provider,
string? ipAddress,
string? userAgent,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,17 @@
using Tiku.Domain.Tenancy;
namespace Tiku.Application.Auth;
public interface ISmsVerificationService
{
Task<SmsSendResult> CreateCodeAsync(
SendSmsCodeRequest request,
CancellationToken cancellationToken = default);
Task VerifyCodeAsync(
Guid tenantId,
string phone,
SmsPurpose purpose,
string code,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,13 @@
using Tiku.Domain.Tenancy;
namespace Tiku.Application.Auth;
public interface ITokenService
{
(string Token, DateTimeOffset ExpiresAt) CreateAccessToken(
Guid userId,
Guid sessionId,
string? phone,
string? email,
TenantMembership membership);
}

View File

@@ -0,0 +1,10 @@
namespace Tiku.Application.Security;
public sealed class JwtOptions
{
public string Issuer { get; set; } = "tiku-backend";
public string Audience { get; set; } = "tiku-api";
public string SigningKey { get; set; } = "development-only-tiku-signing-key-change-before-production";
public int AccessTokenMinutes { get; set; } = 30;
public int RefreshTokenDays { get; set; } = 30;
}