using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using SimpleToolkit.Extensions; using WatchIt.Common.Model.Accounts; using WatchIt.Database; using WatchIt.Database.Model.Account; using WatchIt.WebAPI.Services.Controllers.Common; using WatchIt.WebAPI.Services.Utility.Tokens; using WatchIt.WebAPI.Services.Utility.Tokens.Exceptions; using WatchIt.WebAPI.Services.Utility.User; using Account = WatchIt.Database.Model.Account.Account; using AccountProfilePicture = WatchIt.Common.Model.Accounts.AccountProfilePicture; namespace WatchIt.WebAPI.Services.Controllers.Accounts; public class AccountsControllerService( ILogger logger, DatabaseContext database, ITokensService tokensService, IUserService userService ) : IAccountsControllerService { #region PUBLIC METHODS public async Task Register(RegisterRequest data) { string leftSalt = StringExtensions.CreateRandom(20); string rightSalt = StringExtensions.CreateRandom(20); byte[] hash = ComputeHash(data.Password, leftSalt, rightSalt); Account account = new Account { Username = data.Username, Email = data.Email, Password = hash, LeftSalt = leftSalt, RightSalt = rightSalt, }; await database.Accounts.AddAsync(account); await database.SaveChangesAsync(); logger.LogInformation($"New account with ID {account.Id} was created (username: {account.Username}; email: {account.Email})"); return RequestResult.Created($"accounts/{account.Id}", new RegisterResponse(account)); } public async Task Authenticate(AuthenticateRequest data) { Account? account = await database.Accounts.FirstOrDefaultAsync(x => string.Equals(x.Email, data.UsernameOrEmail) || string.Equals(x.Username, data.UsernameOrEmail)); if (account is null || !ComputeHash(data.Password, account.LeftSalt, account.RightSalt).SequenceEqual(account.Password)) { return RequestResult.Unauthorized(); } Task refreshTokenTask = tokensService.CreateRefreshTokenAsync(account, true); Task accessTokenTask = tokensService.CreateAccessTokenAsync(account); AuthenticateResponse response = new AuthenticateResponse { AccessToken = await accessTokenTask, RefreshToken = await refreshTokenTask, }; account.LastActive = DateTime.UtcNow; await database.SaveChangesAsync(); logger.LogInformation($"Account with ID {account.Id} was authenticated"); return RequestResult.Ok(response); } public async Task AuthenticateRefresh() { Guid jti = userService.GetJti(); AccountRefreshToken? token = await database.AccountRefreshTokens.FirstOrDefaultAsync(x => x.Id == jti); if (token is null || token.ExpirationDate < DateTime.UtcNow) { return RequestResult.Unauthorized(); } string refreshToken; try { refreshToken = await tokensService.ExtendRefreshTokenAsync(token.Account, token.Id); } catch (TokenNotFoundException) { return RequestResult.Unauthorized(); } catch (TokenNotExtendableException) { refreshToken = userService.GetRawToken().Replace("Bearer ", string.Empty); } string accessToken = await tokensService.CreateAccessTokenAsync(token.Account); token.Account.LastActive = DateTime.UtcNow; await database.SaveChangesAsync(); logger.LogInformation($"Account with ID {token.AccountId} was authenticated by token refreshing"); return RequestResult.Ok(new AuthenticateResponse { AccessToken = accessToken, RefreshToken = refreshToken, }); } public async Task Logout() { Guid jti = userService.GetJti(); AccountRefreshToken? token = await database.AccountRefreshTokens.FirstOrDefaultAsync(x => x.Id == jti); if (token is not null) { database.AccountRefreshTokens.Attach(token); database.AccountRefreshTokens.Remove(token); await database.SaveChangesAsync(); } return RequestResult.NoContent(); } public async Task GetAccountProfilePicture(long id) { Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == id); if (account is null) { return RequestResult.BadRequest() .AddValidationError("id", "Account with this id does not exists"); } if (account.ProfilePicture is null) { return RequestResult.NotFound(); } AccountProfilePictureResponse picture = new AccountProfilePictureResponse(account.ProfilePicture); return RequestResult.Ok(picture); } public async Task GetAccountInfo() => await GetAccountInfo(userService.GetUserId()); public async Task GetAccountInfo(long id) { Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == id); if (account is null) { return RequestResult.NotFound(); } AccountResponse response = new AccountResponse(account); return RequestResult.Ok(response); } public async Task PutAccountInfo(AccountRequest data) { Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == userService.GetUserId()); if (account is null) { return RequestResult.NotFound(); } data.UpdateAccount(account); return RequestResult.Ok(); } #endregion #region PRIVATE METHODS protected byte[] ComputeHash(string password, string leftSalt, string rightSalt) => SHA512.HashData(Encoding.UTF8.GetBytes($"{leftSalt}{password}{rightSalt}")); #endregion }