module Server.Auth open System open System.Security.Claims open System.Text open System.IdentityModel.Tokens.Jwt open Microsoft.IdentityModel.Tokens open Domain let private issuer = "quizsystem" let private audience = "quizsystem-client" let private signingKey (secret: string) = SymmetricSecurityKey(Encoding.UTF8.GetBytes secret) let issueToken (secret: string) (user: User) : string = let creds = SigningCredentials(signingKey secret, SecurityAlgorithms.HmacSha256) let (UserId rawId) = user.Id let claims = [| Claim(JwtRegisteredClaimNames.Sub, string rawId) Claim(ClaimTypes.Name, user.Name) Claim(ClaimTypes.Email, user.Email) Claim(ClaimTypes.Role, string user.Role) |] let token = JwtSecurityToken( issuer = issuer, audience = audience, claims = claims, expires = DateTime.UtcNow.AddHours 8.0, signingCredentials = creds ) JwtSecurityTokenHandler().WriteToken token let tokenValidationParameters (secret: string) = TokenValidationParameters( ValidateIssuer = true, ValidIssuer = issuer, ValidateAudience = true, ValidAudience = audience, ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey secret, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes 1.0 ) let tryGetUserId (principal: ClaimsPrincipal) : UserId option = match principal.FindFirst(JwtRegisteredClaimNames.Sub) with | null -> None | claim -> match Guid.TryParse claim.Value with | true, guid -> Some(UserId guid) | false, _ -> None /// Shared by every handler that requires a signed-in user, so each feature /// doesn't repeat its own "Требуется авторизация" error text. let requireUserId (principal: ClaimsPrincipal) : Result = match tryGetUserId principal with | Some uid -> Ok uid | None -> Error "Требуется авторизация" let private tryGetRole (principal: ClaimsPrincipal) : Role option = match principal.FindFirst(ClaimTypes.Role) with | null -> None | claim -> match claim.Value with | "Admin" -> Some Admin | "Teacher" -> Some Teacher | "Student" -> Some Student | _ -> None /// Like `requireUserId`, but also checks the JWT's role claim is one of /// `allowed` — used by Teacher/Admin-only handlers. let requireRole (allowed: Role list) (principal: ClaimsPrincipal) : Result = match tryGetUserId principal, tryGetRole principal with | Some uid, Some role when List.contains role allowed -> Ok uid | Some _, Some _ -> Error "Доступ запрещён" | _ -> Error "Требуется авторизация"