Sign-up stays closed (invitations only), so a new server's first login could only be made over SSH with admin create-root, and an invitation needs a group, which needs a login. While no administrator exists, GET /clientapi/user/setup says so and POST takes a SetupForm (login, password, first persona): the login is made the administrator with its persona, and setup closes for good (owner decision 2026-10-16: first come, first served, no setup code). ServerSetup is one document with a fixed id, so the first request to insert it wins and two setups at once make one administrator. A setup that fails before its login is made gives the claim back; one that named an administrator never does, whoever is demoted or deleted later, and the CLI stays the way back. admin promote and create-root --admin seal it too, migration _018 seals a server that already had an administrator, and no backup holds it nor does a restore drop it, since a restore tombstones an administrator made after the backup. Public sign-up, NodeInfo and the instance API are unchanged. RootUser.SetAdmin is the one place an administrator is made (with isModerator, which IsAdminPolicy asks for), and the invitation endpoints share InsertPersona with setup. Tests: ServerSetupTests on the invitations-only host (setup, the race, failed setups, sign-up still closed, the CLI's seal) and the migration; the whole suite passes (962). In the pasture, decePub's setup and invitation pages pass end to end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
534 lines
20 KiB
C#
534 lines
20 KiB
C#
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.RateLimiting;
|
|
using Microsoft.Extensions.Localization;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
using MongoDB.Entities;
|
|
|
|
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.Infrastructure;
|
|
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> AppConfiguration;
|
|
readonly IOptionsMonitor<RegistrationOptions> Registrations;
|
|
readonly ILogger<RootUserController> Logger;
|
|
readonly IStringLocalizer Localizer;
|
|
|
|
public RootUserController(IRootUsersService usersService,
|
|
IGroupUsersService groupUsersService,
|
|
IPrivateAvatarUsersService avatarUsersService,
|
|
ILocalActorService localActors,
|
|
AuthTokenManager authTokenManager,
|
|
IOptionsMonitor<AppConfiguration> appConfiguration,
|
|
IOptionsMonitor<RegistrationOptions> registrations,
|
|
IStringLocalizer<GenericRes> localizer,
|
|
ILogger<RootUserController> logger)
|
|
{
|
|
Registrations = registrations;
|
|
UsersService = usersService;
|
|
GroupUsersService = groupUsersService;
|
|
AvatarUsersService = avatarUsersService;
|
|
LocalActors = localActors;
|
|
AuthTokenManager = authTokenManager;
|
|
AppConfiguration = appConfiguration;
|
|
Localizer = localizer;
|
|
Logger = logger;
|
|
}
|
|
|
|
#region User endpoints
|
|
|
|
[HttpPost, Route("/clientapi/user/signup"), EnableRateLimiting(RateLimiting.Accounts), Authorize(Policy = Policies.IsUser), AllowAnonymous]
|
|
public async Task<IActionResult> SignUp(LoginForm signUpForm)
|
|
{
|
|
if (User.Identity?.IsAuthenticated ?? false) return Redirect("/");
|
|
var result = new WebResult();
|
|
if (!Registrations.CurrentValue.IsOpen)
|
|
return StatusCode(StatusCodes.Status403Forbidden, result.Invalidate(Localizer[RegistrationOptions.ClosedMessage], StatusCodes.Status403Forbidden));
|
|
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"), EnableRateLimiting(RateLimiting.Accounts), Authorize(Policy = Policies.IsUser), AllowAnonymous]
|
|
public async Task<IActionResult> 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"), EnableRateLimiting(RateLimiting.Accounts), AllowAnonymous]
|
|
public async Task<IActionResult> 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 InsertPersona(user.ID, avatarUserName, signUpForm.AvatarName);
|
|
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"), EnableRateLimiting(RateLimiting.Accounts), AllowAnonymous]
|
|
public async Task<IActionResult> 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<ViewAvatar>)?.FirstOrDefault()
|
|
: (avatars.Data as List<ViewAvatar>)?.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 InsertPersona(user.ID, avatarUserName, loginForm.AvatarName);
|
|
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."]));
|
|
}
|
|
}
|
|
|
|
[HttpGet, Route("/clientapi/user/setup"), AllowAnonymous]
|
|
public async Task<IActionResult> Setup(CancellationToken token) =>
|
|
Ok(new ViewServerSetup { IsPending = await ServerSetup.IsPending(token) });
|
|
|
|
// The server's first login, made while it has no administrator (owner decision 2026-10-16: first come, first served):
|
|
// it is made the administrator, with its first persona, and setup closes for good. Sign-up stays closed to everyone
|
|
// else (Registrations), so NodeInfo and the instance API don't change.
|
|
[HttpPost, Route("/clientapi/user/setup"), EnableRateLimiting(RateLimiting.Accounts), AllowAnonymous]
|
|
public async Task<IActionResult> SetUp(SetupForm setupForm, CancellationToken token)
|
|
{
|
|
if (User.Identity?.IsAuthenticated ?? false) return Redirect("/");
|
|
var result = new WebResult();
|
|
if (!ModelState.IsValid)
|
|
return BadRequest(result.Invalidate(Localizer["Invalid model."]));
|
|
var claimed = false;
|
|
var adminId = default(string);
|
|
try
|
|
{
|
|
if (!await ServerSetup.IsPending(token))
|
|
return SetUpAlready();
|
|
|
|
var avatarUserName = setupForm.AvatarUserName?.Trim().ToLowerInvariant();
|
|
var personaProblem = await PersonaProblem(avatarUserName, setupForm.UserName, token);
|
|
if (personaProblem != default)
|
|
return BadRequest(result.Invalidate(personaProblem));
|
|
|
|
claimed = await ServerSetup.Claim(token);
|
|
if (!claimed)
|
|
return SetUpAlready();
|
|
|
|
result = await UsersService.SignUpAsync(setupForm);
|
|
if (!result.IsValid)
|
|
{
|
|
await ServerSetup.Release();
|
|
return StatusCode(result.StatusCode, result);
|
|
}
|
|
var (user, userSettings) = ((RootUser, ViewUserSettings))result.Data;
|
|
user.SetAdmin(true);
|
|
await DB.Default.SaveAsync(user, CancellationToken.None);
|
|
adminId = user.ID;
|
|
await ServerSetup.Claimed(adminId, CancellationToken.None);
|
|
|
|
// set up from here, whatever happens to the persona: it can be made in the client's settings
|
|
var avatar = await InsertPersona(user.ID, avatarUserName, setupForm.AvatarName);
|
|
if (!avatar.IsValid)
|
|
return StatusCode(avatar.StatusCode, avatar);
|
|
|
|
return Ok(AuthTokenManager.GenerateToken(user, userSettings));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, $"{nameof(User)}.{nameof(SetUp)}()");
|
|
if (claimed && adminId == default)
|
|
await ServerSetup.Release();
|
|
return BadRequest(result.Invalidate(Localizer["Something went wrong."]));
|
|
}
|
|
}
|
|
|
|
IActionResult SetUpAlready() =>
|
|
StatusCode(StatusCodes.Status403Forbidden,
|
|
new WebResult().Invalidate(Localizer["This server is already set up: joining it takes a group invitation."], StatusCodes.Status403Forbidden));
|
|
|
|
Task<WebResult> InsertPersona(string rootId, string avatarUserName, string avatarName) =>
|
|
AvatarUsersService.InsertAvatar(new InsertAvatarForm
|
|
{
|
|
RootId = rootId,
|
|
Name = string.IsNullOrWhiteSpace(avatarName) ? avatarUserName : avatarName.Trim(),
|
|
UserName = avatarUserName,
|
|
Biography = string.Empty
|
|
});
|
|
|
|
async Task<string> 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 taken.", 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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."]));
|
|
// }
|
|
//}
|
|
|
|
// what the login's media take, every persona's, and its quota when the server sets one
|
|
[HttpGet, Route("/clientapi/user/storage"), Authorize(Policy = Policies.IsUser)]
|
|
public async Task<IActionResult> GetStorage([FromServices] Microsoft.Extensions.Options.IOptionsMonitor<Domain.Media.MediaOptions> media, CancellationToken token)
|
|
{
|
|
var (bytes, files) = await Domain.Privacy.Counted.MediaOfRoot(User.GetUserId(), token);
|
|
var quota = media.CurrentValue.QuotaBytesPerRoot;
|
|
return Ok(new PrivaPub.ClientModels.User.ViewStorage { MediaBytes = bytes, MediaFiles = files, QuotaBytes = quota > 0 ? quota : null });
|
|
}
|
|
|
|
[HttpGet, Route("/clientapi/user/settings"), Authorize(Policy = Policies.IsUser)]
|
|
public async Task<IActionResult> 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"), EnableRateLimiting(RateLimiting.Accounts), AllowAnonymous]
|
|
public async Task<IActionResult> 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.Data);//the same sentence for everyone
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, $"{nameof(User)}.{nameof(RecoverPassword)}()");
|
|
return BadRequest(result.Invalidate(Localizer["Something went wrong."]));
|
|
}
|
|
}
|
|
|
|
[HttpPost, Route("/clientapi/user/recover/valid"), EnableRateLimiting(RateLimiting.Accounts), AllowAnonymous]
|
|
public async Task<IActionResult> 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"), EnableRateLimiting(RateLimiting.Accounts), AllowAnonymous]
|
|
public async Task<IActionResult> 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."]));
|
|
}
|
|
}
|
|
|
|
[HttpPost, Route("/clientapi/user/delete"), EnableRateLimiting(RateLimiting.Accounts), Authorize(Policy = Policies.IsUser)]
|
|
public async Task<IActionResult> RemoveSelf(RemoveSelfForm form)
|
|
{
|
|
var result = new WebResult();
|
|
if (!ModelState.IsValid)
|
|
return BadRequest(result.Invalidate(Localizer["Invalid model."]));
|
|
try
|
|
{
|
|
result = await UsersService.RemoveSelfAsync(User.GetUserId(), form.Password);
|
|
return result.IsValid ? Ok() : StatusCode(result.StatusCode, result);
|
|
}
|
|
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<IActionResult> SniffAgain([FromServices] DbEntities dbEntities, CancellationToken token)
|
|
{
|
|
var result = new WebResult();
|
|
try
|
|
{
|
|
var user = await dbEntities.RootUsers.MatchID(User.GetUserId()).ExecuteFirstAsync(token);
|
|
if (user is not { IsBanned: false, DeletedAt: null })
|
|
return Unauthorized(result.Invalidate(Localizer["Unauthorized."], StatusCodes.Status401Unauthorized));
|
|
result = await UsersService.GetUserSettingsAsync(user.ID);
|
|
if (!result.IsValid)
|
|
return StatusCode(result.StatusCode, result);
|
|
|
|
return Ok(AuthTokenManager.GenerateToken(user, (ViewUserSettings)result.Data));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, $"{nameof(User)}.{nameof(SniffAgain)}()");
|
|
return BadRequest(result.Invalidate(Localizer["Something went wrong."]));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|