using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using PrivaPub.ClientModels; using PrivaPub.ClientModels.Resources; using PrivaPub.ClientModels.Group; using PrivaPub.ClientModels.User; using PrivaPub.ClientModels.User.Avatar; using PrivaPub.Extensions; using PrivaPub.Federation.Actors; using PrivaPub.Models; using PrivaPub.Models.User; using PrivaPub.Resources; using PrivaPub.Services; using PrivaPub.Services.ClientToServer.Private; using PrivaPub.StaticServices; using System.ComponentModel.DataAnnotations; namespace PrivaPub.Controllers.ClientToServer { [ApiController, Route("clientapi/user")] public class RootUserController : ControllerBase { readonly IRootUsersService UsersService; readonly IGroupUsersService GroupUsersService; readonly IPrivateAvatarUsersService AvatarUsersService; readonly ILocalActorService LocalActors; readonly AuthTokenManager AuthTokenManager; readonly IOptionsMonitor AppConfiguration; readonly ILogger Logger; readonly IStringLocalizer Localizer; public RootUserController(IRootUsersService usersService, IGroupUsersService groupUsersService, IPrivateAvatarUsersService avatarUsersService, ILocalActorService localActors, AuthTokenManager authTokenManager, IOptionsMonitor appConfiguration, IStringLocalizer localizer, ILogger logger) { UsersService = usersService; GroupUsersService = groupUsersService; AvatarUsersService = avatarUsersService; LocalActors = localActors; AuthTokenManager = authTokenManager; AppConfiguration = appConfiguration; Localizer = localizer; Logger = logger; } #region User endpoints [HttpPost, Route("/clientapi/user/signup"), Authorize(Policy = Policies.IsUser), AllowAnonymous] public async Task SignUp(LoginForm signUpForm) { if (User.Identity?.IsAuthenticated ?? false) return Redirect("/"); var result = new WebResult(); if (!ModelState.IsValid) return BadRequest(result.Invalidate(Localizer["Invalid model."])); try { result = await UsersService.SignUpAsync(signUpForm); if (!result.IsValid) return StatusCode(result.StatusCode, result); (var user, var userSettings) = ((RootUser, ViewUserSettings))result.Data; var jwtUser = AuthTokenManager.GenerateToken(user, userSettings); return Ok(jwtUser); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(SignUp)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } [HttpPost, Route("/clientapi/user/login"), Authorize(Policy = Policies.IsUser), AllowAnonymous] public async Task Login(LoginForm loginForm) { if (User.Identity?.IsAuthenticated ?? false) return Redirect("/discussions"); var result = new WebResult(); if (!ModelState.IsValid) return BadRequest(result.Invalidate(Localizer["Invalid model."])); try { result = await UsersService.LoginAsync(loginForm); if (!result.IsValid) return StatusCode(result.StatusCode, result); var (user, userSettings) = ((RootUser, ViewUserSettings))result.Data; var jwtUser = AuthTokenManager.GenerateToken(user, userSettings); return Ok(jwtUser); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(Login)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } [HttpPost, Route("/clientapi/user/invitation/signup"), AllowAnonymous] public async Task InvitationSignUp(InvitationLoginForm signUpForm, CancellationToken token) { var result = new WebResult(); if (!ModelState.IsValid) return BadRequest(result.Invalidate(Localizer["Invalid model."])); try { var invitation = await GroupUsersService.GetInvitation(signUpForm.InvitationCode, signUpForm.InvitationPassword, token); if (!invitation.IsValid) return StatusCode(invitation.StatusCode, invitation); var avatarUserName = signUpForm.AvatarUserName?.Trim().ToLowerInvariant(); var personaProblem = await PersonaProblem(avatarUserName, signUpForm.UserName, token); if (personaProblem != default) return BadRequest(result.Invalidate(personaProblem)); result = await UsersService.SignUpAsync(signUpForm, signUpForm.InvitationCode); if (!result.IsValid) return StatusCode(result.StatusCode, result); var (user, userSettings) = ((RootUser, ViewUserSettings))result.Data; var avatar = await AvatarUsersService.InsertAvatar(new InsertAvatarForm { RootId = user.ID, Name = string.IsNullOrWhiteSpace(signUpForm.AvatarName) ? avatarUserName : signUpForm.AvatarName.Trim(), UserName = avatarUserName, Biography = string.Empty }); if (!avatar.IsValid) return StatusCode(avatar.StatusCode, avatar); var joined = await GroupUsersService.JoinGroup(user.ID, new JoinGroupForm { AvatarId = ((ViewAvatar)avatar.Data).Id, InvitationCode = signUpForm.InvitationCode, InvitationPassword = signUpForm.InvitationPassword }, token); if (!joined.IsValid) return StatusCode(joined.StatusCode, joined); return Ok(AuthTokenManager.GenerateToken(user, userSettings)); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(InvitationSignUp)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } [HttpPost, Route("/clientapi/user/invitation/login"), AllowAnonymous] public async Task InvitationLogin(InvitationLoginForm loginForm, CancellationToken token) { var result = new WebResult(); if (!ModelState.IsValid) return BadRequest(result.Invalidate(Localizer["Invalid model."])); try { var invitation = await GroupUsersService.GetInvitation(loginForm.InvitationCode, loginForm.InvitationPassword, token); if (!invitation.IsValid) return StatusCode(invitation.StatusCode, invitation); result = await UsersService.LoginAsync(loginForm, loginForm.InvitationCode); if (!result.IsValid) return StatusCode(result.StatusCode, result); var (user, userSettings) = ((RootUser, ViewUserSettings))result.Data; var avatars = await AvatarUsersService.GetRootAvatars(user.ID, token); var avatarUserName = loginForm.AvatarUserName?.Trim().ToLowerInvariant(); var avatar = string.IsNullOrEmpty(avatarUserName) ? (avatars.Data as List)?.FirstOrDefault() : (avatars.Data as List)?.FirstOrDefault(a => a.UserName == avatarUserName); if (avatar == default) { var personaProblem = await PersonaProblem(avatarUserName, user.UserName, token); if (personaProblem != default) return BadRequest(new WebResult().Invalidate(personaProblem)); var inserted = await AvatarUsersService.InsertAvatar(new InsertAvatarForm { RootId = user.ID, Name = string.IsNullOrWhiteSpace(loginForm.AvatarName) ? avatarUserName : loginForm.AvatarName.Trim(), UserName = avatarUserName, Biography = string.Empty }); if (!inserted.IsValid) return StatusCode(inserted.StatusCode, inserted); avatar = (ViewAvatar)inserted.Data; } var joined = await GroupUsersService.JoinGroup(user.ID, new JoinGroupForm { AvatarId = avatar.Id, InvitationCode = loginForm.InvitationCode, InvitationPassword = loginForm.InvitationPassword }, token); if (!joined.IsValid) return StatusCode(joined.StatusCode, joined); return Ok(AuthTokenManager.GenerateToken(user, userSettings)); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(InvitationLogin)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } async Task PersonaProblem(string avatarUserName, string rootUserName, CancellationToken token) { if (string.IsNullOrEmpty(avatarUserName)) return Localizer["Choose a username for your persona."]; if (string.Equals(avatarUserName, rootUserName?.Trim(), StringComparison.OrdinalIgnoreCase)) return Localizer["Your persona's username must differ from your login."]; if (await LocalActors.IsUserNameTaken(avatarUserName, token)) return Localizer["The username '{0}' is already take.", avatarUserName]; return default; } [HttpGet, Route("/clientapi/user/logout"), Authorize(Policy = Policies.IsUser)] public IActionResult Logout() { var result = new WebResult(); try { return Ok(); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(Logout)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } [HttpPost, Route("/clientapi/user/update"), Authorize(Policy = Policies.IsUser)] public async Task UpdateUser(UserForm userEmailForm) { var result = new WebResult(); if (!ModelState.IsValid) return BadRequest(result.Invalidate(Localizer["Invalid model."])); try { result = await UsersService.UpdateUserAsync(userEmailForm, User.GetUserId()); if (!result.IsValid) return StatusCode(result.StatusCode, result); return Ok(); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(UpdateUser)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } [HttpPost, Route("/clientapi/user/update/settings"), Authorize(Policy = Policies.IsUser)] public async Task UpdateUserSettings(ViewUserSettings userSettings) { var result = new WebResult(); if (!ModelState.IsValid) return BadRequest(result.Invalidate(Localizer["Invalid model."])); try { result = await UsersService.UpdateUserSettingsAsync(userSettings, User.GetUserId()); if (!result.IsValid) return StatusCode(result.StatusCode, result); return Ok(); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(UpdateUserSettings)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } [HttpPost, Route("/clientapi/user/update/password"), Authorize(Policy = Policies.IsUser)] public async Task UpdatePassword(UserPasswordForm userPasswordForm) { var result = new WebResult(); if (!ModelState.IsValid) return BadRequest(result.Invalidate(Localizer["Invalid model."])); try { result = await UsersService.UpdateUserPasswordAsync(userPasswordForm, User.GetUserId()); if (!result.IsValid) return StatusCode(result.StatusCode, result); return Ok(); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(UpdatePassword)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } //[HttpGet, Authorize(Policy = Policies.IsUser)] //public async Task GetUser() //{ // var result = new WebResult(); // try // { // result = await UsersService.GetUserAsync(User.GetUserId()); // if (!result.IsValid) // return StatusCode(result.StatusCode, result); // return Ok(result.Data); // } // catch (Exception ex) // { // Logger.LogError(ex, $"{nameof(User)}.{nameof(GetUser)}()"); // return BadRequest(result.Invalidate(Localizer["Something went wrong."])); // } //} [HttpGet, Route("/clientapi/user/settings"), Authorize(Policy = Policies.IsUser)] public async Task GetUserSettings() { var result = new WebResult(); try { result = await UsersService.GetUserSettingsAsync(User.GetUserId()); if (!result.IsValid) return StatusCode(result.StatusCode, result); return Ok(result.Data); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(GetUserSettings)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } [HttpPost, Route("/clientapi/user/recover/password"), AllowAnonymous] public async Task RecoverPassword(PasswordRecoveryForm passwordRecoveryForm) { var result = new WebResult(); if (!ModelState.IsValid) return BadRequest(result.Invalidate(Localizer["Invalid model."])); try { var host = string.IsNullOrEmpty(AppConfiguration.CurrentValue.FrontendBaseAddress) ? $"{Request.Scheme}://{Request.Host.Host}" : AppConfiguration.CurrentValue.FrontendBaseAddress.TrimEnd('/'); result = await UsersService.SetupAndSendRecoveryEmail(passwordRecoveryForm, host); if (!result.IsValid) return StatusCode(result.StatusCode, result); return Ok(result); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(RecoverPassword)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } [HttpPost, Route("/clientapi/user/recover/valid"), AllowAnonymous] public async Task IsValidRecoveryCode( [FromBody, Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)), Display(Name = "RecoveryCode", ResourceType = typeof(FieldsNameResource))] string recoveryCode) { var result = new WebResult(); try { result = await UsersService.IsValidRecoveryCode(recoveryCode); if (!result.IsValid) return StatusCode(result.StatusCode, result); return Ok(result.Data); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(IsValidRecoveryCode)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } [HttpPost, Route("/clientapi/user/recover/update/password"), AllowAnonymous] public async Task ChangePassword(NewPasswordForm newPasswordForm) { var result = new WebResult(); if (!ModelState.IsValid) return BadRequest(result.Invalidate(Localizer["Invalid model."])); try { result = await UsersService.ChangePassword(newPasswordForm); if (!result.IsValid) return StatusCode(result.StatusCode, result); return Ok(result.Data); } catch (Exception ex) { Logger.LogError(ex, $"{nameof(User)}.{nameof(ChangePassword)}()"); return BadRequest(result.Invalidate(Localizer["Something went wrong."])); } } //[HttpDelete, Route("delete"), Authorize(Policy = Policies.IsUser)] //public async Task RemoveSelf() //{ // var result = new WebResult(); // try // { // //result = await UsersService.RemoveUserAsync(User.GetUserId()); // if (!result.IsValid) // return StatusCode(result.StatusCode, result); // await HttpContext.SignOutAsync(); // return Ok(); // } // catch (Exception ex) // { // Logger.LogError(ex, $"{nameof(User)}.{nameof(RemoveSelf)}()"); // return BadRequest(result.Invalidate(Localizer["Something went wrong."])); // } //} #endregion User endpoints #region Auth refresh [HttpGet, Route("/clientapi/user/sniff/again"), Authorize(Policy = Policies.IsUser)] public async Task SniffAgain() { return Ok(); } #endregion } }