using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using OnlyPrompt.Backend.ApiModels.Auth; using OnlyPrompt.Backend.Database; using OnlyPrompt.Backend.Database.Models; using OnlyPrompt.Backend.Services.Jwt; using OnlyPrompt.Backend.Utils; namespace OnlyPrompt.Backend.Controllers { [ApiController] [Route("api/v1/auth")] public class AuthController : BaseController { private static readonly CookieOptions AuthCookieOptions = new CookieOptions { Secure = true, HttpOnly = true, IsEssential = true }; private readonly IPasswordHasher _passwordHasher; private readonly ITokenService _jwtService; private readonly ILogger _logger; public AuthController(OnlyPromptContext db, IPasswordHasher passwordHasher, IMapper mapper, ILogger logger, ITokenService jwtService) : base(db, mapper) { _passwordHasher=passwordHasher; _logger=logger; _jwtService=jwtService; } [AllowAnonymous] [HttpPost("login")] public async Task, NotFound>> LoginAsync([FromBody] ApiLoginRequest request, [FromQuery]string redirect = null) { var user = await FindUserAsync(request.UserNameOrEmail); if (user is null) return TypedResults.NotFound("User not found"); var verificationResult = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password); if (verificationResult == PasswordVerificationResult.Failed) return TypedResults.NotFound("User not found"); // Don't reveal that the user exists if (user.IsLockoutEnabled) return TypedResults.BadRequest("User is locked out"); // Don't reveal that the user exists var token = _jwtService.BuildToken(user, out var validUntil); this.Response.Cookies.Append("jwt", token, AuthCookieOptions.Copy(c => c.Expires = validUntil)); if (string.IsNullOrEmpty(redirect) == false) return TypedResults.Redirect(redirect, false); return TypedResults.Ok(); } [AllowAnonymous] [HttpPost("register")] public async Task>> RegisterAsync([FromBody] ApiRegisterRequest request, [FromQuery] string redirect = null) { var existingUser = await FindUserAsync(request.UserName, request.Email); if (existingUser is not null) { var errors = new Dictionary(); if (existingUser.UserName == request.UserName) errors.Add(nameof(request.UserName), ["Username is already taken"]); if (existingUser.Email == request.Email) errors.Add(nameof(request.Email), ["Email is already registered"]); return TypedResults.ValidationProblem(errors); } var id = Guid.CreateVersion7(); var slug = await SlugHelper.GenerateUniqueSlugAsync(request.UserName, s => _db.UserProfiles.AnyAsync(up => up.Slug == s), 32); var avatarUrl = $"https://api.dicebear.com/9.x/bottts/svg?seed={id}"; var newUser = new UserModel { Id = id, Profile = new UserProfileModel { AvatarUrl = avatarUrl, DisplayName = request.DisplayName, Slug = slug, }, Roles = [ModelConstants.UserRole], PasswordHash = null, UserName = request.UserName ?? request.Email, Email = request.Email, IsLockoutEnabled = false, }; newUser.PasswordHash = _passwordHasher.HashPassword(newUser, request.Password); _db.Users.Add(newUser); await _db.SaveChangesAsync(); if(string.IsNullOrEmpty(redirect) == false) return TypedResults.Redirect(redirect, false); return TypedResults.Ok(_mapper.Map(newUser)); } [HttpPost("logout")] public RedirectHttpResult Logout() { this.Response.Cookies.Delete("jwt", AuthCookieOptions); return TypedResults.Redirect("login"); } } }