mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-11-15 13:27:54 +01:00
OAuth2 (Discord)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
using VoidCat.Services.Files;
|
using VoidCat.Services.Files;
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ public class AdminController : Controller
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("update-user")]
|
[Route("update-user")]
|
||||||
public async Task<IActionResult> UpdateUser([FromBody] PrivateVoidUser user)
|
public async Task<IActionResult> UpdateUser([FromBody] PrivateUser user)
|
||||||
{
|
{
|
||||||
var oldUser = await _userStore.Get(user.Id);
|
var oldUser = await _userStore.Get(user.Id);
|
||||||
if (oldUser == default) return BadRequest();
|
if (oldUser == default) return BadRequest();
|
||||||
@@ -101,5 +102,5 @@ public class AdminController : Controller
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
public record AdminListedUser(PrivateVoidUser User, int Uploads);
|
public record AdminListedUser(PrivateUser User, int Uploads);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,27 +5,30 @@ using System.Text;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
using VoidCat.Services.Users;
|
||||||
|
|
||||||
namespace VoidCat.Controllers;
|
namespace VoidCat.Controllers;
|
||||||
|
|
||||||
[Route("auth")]
|
[Route("auth")]
|
||||||
public class AuthController : Controller
|
public class AuthController : Controller
|
||||||
{
|
{
|
||||||
private readonly IUserManager _manager;
|
private readonly UserManager _manager;
|
||||||
private readonly VoidSettings _settings;
|
private readonly VoidSettings _settings;
|
||||||
private readonly ICaptchaVerifier _captchaVerifier;
|
private readonly ICaptchaVerifier _captchaVerifier;
|
||||||
private readonly IApiKeyStore _apiKeyStore;
|
private readonly IApiKeyStore _apiKeyStore;
|
||||||
private readonly IUserStore _userStore;
|
private readonly IUserStore _userStore;
|
||||||
|
|
||||||
public AuthController(IUserManager userStore, VoidSettings settings, ICaptchaVerifier captchaVerifier, IApiKeyStore apiKeyStore,
|
public AuthController(UserManager userManager, VoidSettings settings, ICaptchaVerifier captchaVerifier,
|
||||||
IUserStore userStore1)
|
IApiKeyStore apiKeyStore,
|
||||||
|
IUserStore userStore)
|
||||||
{
|
{
|
||||||
_manager = userStore;
|
_manager = userManager;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_captchaVerifier = captchaVerifier;
|
_captchaVerifier = captchaVerifier;
|
||||||
_apiKeyStore = apiKeyStore;
|
_apiKeyStore = apiKeyStore;
|
||||||
_userStore = userStore1;
|
_userStore = userStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -96,6 +99,35 @@ public class AuthController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start OAuth2 authorize flow
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider">OAuth provider</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{provider}")]
|
||||||
|
public IActionResult Authorize([FromRoute] string provider)
|
||||||
|
{
|
||||||
|
return Redirect(_manager.Authorize(provider).ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authorize user from OAuth2 code grant
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">Code used to generate access token</param>
|
||||||
|
/// <param name="provider">OAuth provider</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{provider}/token")]
|
||||||
|
public async Task<IActionResult> Token([FromRoute] string provider, [FromQuery] string code)
|
||||||
|
{
|
||||||
|
var newUser = await _manager.LoginOrRegister(code, provider);
|
||||||
|
var token = CreateToken(newUser, DateTime.UtcNow.AddHours(12));
|
||||||
|
var tokenWriter = new JwtSecurityTokenHandler();
|
||||||
|
|
||||||
|
return Redirect($"/login#{tokenWriter.WriteToken(token)}");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List api keys for the user
|
/// List api keys for the user
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -145,7 +177,7 @@ public class AuthController : Controller
|
|||||||
return Json(key);
|
return Json(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private JwtSecurityToken CreateToken(VoidUser user, DateTime expiry)
|
private JwtSecurityToken CreateToken(User user, DateTime expiry)
|
||||||
{
|
{
|
||||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.JwtSettings.Key));
|
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.JwtSettings.Key));
|
||||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||||
@@ -171,19 +203,14 @@ public class AuthController : Controller
|
|||||||
Password = password;
|
Password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Required]
|
[Required] [EmailAddress] public string Username { get; }
|
||||||
[EmailAddress]
|
|
||||||
public string Username { get; }
|
|
||||||
|
|
||||||
[Required]
|
[Required] [MinLength(6)] public string Password { get; }
|
||||||
[MinLength(6)]
|
|
||||||
public string Password { get; }
|
|
||||||
|
|
||||||
public string? Captcha { get; init; }
|
public string? Captcha { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record LoginResponse(string? Jwt, string? Error = null, VoidUser? Profile = null);
|
public sealed record LoginResponse(string? Jwt, string? Error = null, User? Profile = null);
|
||||||
|
|
||||||
|
|
||||||
public sealed record CreateApiKeyRequest(DateTime Expiry);
|
public sealed record CreateApiKeyRequest(DateTime Expiry);
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.StaticFiles;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
using VoidCat.Model.Payments;
|
using VoidCat.Model.Payments;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
using VoidCat.Services.Files;
|
using VoidCat.Services.Files;
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ namespace VoidCat.Controllers
|
|||||||
var store = _settings.DefaultFileStore;
|
var store = _settings.DefaultFileStore;
|
||||||
if (uid.HasValue)
|
if (uid.HasValue)
|
||||||
{
|
{
|
||||||
var user = await _userStore.Get<InternalVoidUser>(uid.Value);
|
var user = await _userStore.Get<InternalUser>(uid.Value);
|
||||||
if (user?.Storage != default)
|
if (user?.Storage != default)
|
||||||
{
|
{
|
||||||
store = user.Storage!;
|
store = user.Storage!;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
using VoidCat.Services.Files;
|
using VoidCat.Services.Files;
|
||||||
|
|
||||||
@@ -42,14 +43,14 @@ public class UserController : Controller
|
|||||||
var requestedId = isMe ? loggedUser!.Value : id.FromBase58Guid();
|
var requestedId = isMe ? loggedUser!.Value : id.FromBase58Guid();
|
||||||
if (loggedUser == requestedId)
|
if (loggedUser == requestedId)
|
||||||
{
|
{
|
||||||
var pUser = await _store.Get<PrivateVoidUser>(requestedId);
|
var pUser = await _store.Get<PrivateUser>(requestedId);
|
||||||
if (pUser == default) return NotFound();
|
if (pUser == default) return NotFound();
|
||||||
|
|
||||||
return Json(pUser);
|
return Json(pUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await _store.Get<PublicVoidUser>(requestedId);
|
var user = await _store.Get<PublicUser>(requestedId);
|
||||||
if (!(user?.Flags.HasFlag(VoidUserFlags.PublicProfile) ?? false)) return NotFound();
|
if (!(user?.Flags.HasFlag(UserFlags.PublicProfile) ?? false)) return NotFound();
|
||||||
|
|
||||||
return Json(user);
|
return Json(user);
|
||||||
}
|
}
|
||||||
@@ -62,12 +63,12 @@ public class UserController : Controller
|
|||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> UpdateUser([FromRoute] string id, [FromBody] PublicVoidUser user)
|
public async Task<IActionResult> UpdateUser([FromRoute] string id, [FromBody] PublicUser user)
|
||||||
{
|
{
|
||||||
var loggedUser = await GetAuthorizedUser(id);
|
var loggedUser = await GetAuthorizedUser(id);
|
||||||
if (loggedUser == default) return Unauthorized();
|
if (loggedUser == default) return Unauthorized();
|
||||||
|
|
||||||
if (!loggedUser.Flags.HasFlag(VoidUserFlags.EmailVerified)) return Forbid();
|
if (!loggedUser.Flags.HasFlag(UserFlags.EmailVerified)) return Forbid();
|
||||||
|
|
||||||
await _store.UpdateProfile(user);
|
await _store.UpdateProfile(user);
|
||||||
return Ok();
|
return Ok();
|
||||||
@@ -97,7 +98,7 @@ public class UserController : Controller
|
|||||||
// not logged in user files, check public flag
|
// not logged in user files, check public flag
|
||||||
var canViewUploads = loggedUser == user.Id || isAdmin;
|
var canViewUploads = loggedUser == user.Id || isAdmin;
|
||||||
if (!canViewUploads &&
|
if (!canViewUploads &&
|
||||||
!user.Flags.HasFlag(VoidUserFlags.PublicUploads)) return Forbid();
|
!user.Flags.HasFlag(UserFlags.PublicUploads)) return Forbid();
|
||||||
|
|
||||||
var results = await _userUploads.ListFiles(id.FromBase58Guid(), request);
|
var results = await _userUploads.ListFiles(id.FromBase58Guid(), request);
|
||||||
var files = await results.Results.ToListAsync();
|
var files = await results.Results.ToListAsync();
|
||||||
@@ -123,7 +124,7 @@ public class UserController : Controller
|
|||||||
var user = await GetAuthorizedUser(id);
|
var user = await GetAuthorizedUser(id);
|
||||||
if (user == default) return Unauthorized();
|
if (user == default) return Unauthorized();
|
||||||
|
|
||||||
var isEmailVerified = (user?.Flags.HasFlag(VoidUserFlags.EmailVerified) ?? false);
|
var isEmailVerified = (user?.Flags.HasFlag(UserFlags.EmailVerified) ?? false);
|
||||||
if (isEmailVerified) return UnprocessableEntity();
|
if (isEmailVerified) return UnprocessableEntity();
|
||||||
|
|
||||||
await _emailVerification.SendNewCode(user!);
|
await _emailVerification.SendNewCode(user!);
|
||||||
@@ -146,22 +147,22 @@ public class UserController : Controller
|
|||||||
var token = code.FromBase58Guid();
|
var token = code.FromBase58Guid();
|
||||||
if (!await _emailVerification.VerifyCode(user, token)) return BadRequest();
|
if (!await _emailVerification.VerifyCode(user, token)) return BadRequest();
|
||||||
|
|
||||||
user.Flags |= VoidUserFlags.EmailVerified;
|
user.Flags |= UserFlags.EmailVerified;
|
||||||
await _store.UpdateProfile(user.ToPublic());
|
await _store.UpdateProfile(user.ToPublic());
|
||||||
return Accepted();
|
return Accepted();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<InternalVoidUser?> GetAuthorizedUser(string id)
|
private async Task<InternalUser?> GetAuthorizedUser(string id)
|
||||||
{
|
{
|
||||||
var loggedUser = HttpContext.GetUserId();
|
var loggedUser = HttpContext.GetUserId();
|
||||||
var gid = id.FromBase58Guid();
|
var gid = id.FromBase58Guid();
|
||||||
var user = await _store.Get<InternalVoidUser>(gid);
|
var user = await _store.Get<InternalUser>(gid);
|
||||||
return user?.Id != loggedUser ? default : user;
|
return user?.Id != loggedUser ? default : user;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<InternalVoidUser?> GetRequestedUser(string id)
|
private async Task<InternalUser?> GetRequestedUser(string id)
|
||||||
{
|
{
|
||||||
var gid = id.FromBase58Guid();
|
var gid = id.FromBase58Guid();
|
||||||
return await _store.Get<InternalVoidUser>(gid);
|
return await _store.Get<InternalUser>(gid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Amazon;
|
|||||||
using Amazon.Runtime;
|
using Amazon.Runtime;
|
||||||
using Amazon.S3;
|
using Amazon.S3;
|
||||||
using VoidCat.Model.Exceptions;
|
using VoidCat.Model.Exceptions;
|
||||||
|
using VoidCat.Model.User;
|
||||||
|
|
||||||
namespace VoidCat.Model;
|
namespace VoidCat.Model;
|
||||||
|
|
||||||
@@ -219,8 +220,18 @@ public static class Extensions
|
|||||||
throw new ArgumentException("Unknown algo", nameof(algo));
|
throw new ArgumentException("Unknown algo", nameof(algo));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool CheckPassword(this InternalVoidUser vu, string password)
|
/// <summary>
|
||||||
|
/// Validate password matches hashed password
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="vu"></param>
|
||||||
|
/// <param name="password"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public static bool CheckPassword(this InternalUser vu, string password)
|
||||||
{
|
{
|
||||||
|
if (vu.AuthType != AuthType.Internal)
|
||||||
|
throw new InvalidOperationException("User type is not internal, cannot check password!");
|
||||||
|
|
||||||
var hashParts = vu.Password.Split(":");
|
var hashParts = vu.Password.Split(":");
|
||||||
return vu.Password == password.Hash(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
|
return vu.Password == password.Hash(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
|
||||||
}
|
}
|
||||||
@@ -239,4 +250,7 @@ public static class Extensions
|
|||||||
|
|
||||||
public static bool HasPlausible(this VoidSettings settings)
|
public static bool HasPlausible(this VoidSettings settings)
|
||||||
=> settings.PlausibleAnalytics?.Endpoint != null;
|
=> settings.PlausibleAnalytics?.Endpoint != null;
|
||||||
|
|
||||||
|
public static bool HasDiscord(this VoidSettings settings)
|
||||||
|
=> settings.Discord != null;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace VoidCat.Model;
|
namespace VoidCat.Model.User;
|
||||||
|
|
||||||
public sealed class ApiKey
|
public sealed class ApiKey
|
||||||
{
|
{
|
||||||
27
VoidCat/Model/User/AuthType.cs
Normal file
27
VoidCat/Model/User/AuthType.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace VoidCat.Model.User;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User account authentication type
|
||||||
|
/// </summary>
|
||||||
|
public enum AuthType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Encrypted password
|
||||||
|
/// </summary>
|
||||||
|
Internal = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PGP challenge
|
||||||
|
/// </summary>
|
||||||
|
PGP = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OAuth2 token
|
||||||
|
/// </summary>
|
||||||
|
OAuth2 = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lightning node challenge
|
||||||
|
/// </summary>
|
||||||
|
Lightning = 3
|
||||||
|
}
|
||||||
12
VoidCat/Model/User/InternalUser.cs
Normal file
12
VoidCat/Model/User/InternalUser.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace VoidCat.Model.User;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal user object used by the system
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InternalUser : PrivateUser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A password hash for the user in the format <see cref="Extensions.HashPassword"/>
|
||||||
|
/// </summary>
|
||||||
|
public string Password { get; init; } = null!;
|
||||||
|
}
|
||||||
17
VoidCat/Model/User/PrivateUser.cs
Normal file
17
VoidCat/Model/User/PrivateUser.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace VoidCat.Model.User;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A user object which includes the Email
|
||||||
|
/// </summary>
|
||||||
|
public class PrivateUser : User
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Users email address
|
||||||
|
/// </summary>
|
||||||
|
public string Email { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Users storage system for new uploads
|
||||||
|
/// </summary>
|
||||||
|
public string? Storage { get; set; }
|
||||||
|
}
|
||||||
6
VoidCat/Model/User/PublicUser.cs
Normal file
6
VoidCat/Model/User/PublicUser.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace VoidCat.Model.User;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public sealed class PublicUser : User
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
// ReSharper disable InconsistentNaming
|
namespace VoidCat.Model.User;
|
||||||
|
|
||||||
namespace VoidCat.Model;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base user object for the system
|
/// The base user object for the system
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class VoidUser
|
public abstract class User
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unique Id of the user
|
/// Unique Id of the user
|
||||||
@@ -43,13 +41,18 @@ public abstract class VoidUser
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Profile flags
|
/// Profile flags
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public VoidUserFlags Flags { get; set; } = VoidUserFlags.PublicProfile;
|
public UserFlags Flags { get; set; } = UserFlags.PublicProfile;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Account authentication type
|
||||||
|
/// </summary>
|
||||||
|
public AuthType AuthType { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the Public object for this user
|
/// Returns the Public object for this user
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public PublicVoidUser ToPublic()
|
public PublicUser ToPublic()
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
@@ -61,44 +64,4 @@ public abstract class VoidUser
|
|||||||
Flags = Flags
|
Flags = Flags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Internal user object used by the system
|
|
||||||
/// </summary>
|
|
||||||
public sealed class InternalVoidUser : PrivateVoidUser
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A password hash for the user in the format <see cref="Extensions.HashPassword"/>
|
|
||||||
/// </summary>
|
|
||||||
public string Password { get; init; } = null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A user object which includes the Email
|
|
||||||
/// </summary>
|
|
||||||
public class PrivateVoidUser : VoidUser
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Users email address
|
|
||||||
/// </summary>
|
|
||||||
public string Email { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Users storage system for new uploads
|
|
||||||
/// </summary>
|
|
||||||
public string? Storage { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public sealed class PublicVoidUser : VoidUser
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum VoidUserFlags
|
|
||||||
{
|
|
||||||
PublicProfile = 1,
|
|
||||||
PublicUploads = 2,
|
|
||||||
EmailVerified = 4
|
|
||||||
}
|
}
|
||||||
23
VoidCat/Model/User/UserAuthToken.cs
Normal file
23
VoidCat/Model/User/UserAuthToken.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace VoidCat.Model.User;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OAuth2 access token
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UserAuthToken
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
|
||||||
|
public Guid User { get; init; }
|
||||||
|
|
||||||
|
public string Provider { get; init; }
|
||||||
|
|
||||||
|
public string AccessToken { get; init; }
|
||||||
|
|
||||||
|
public string TokenType { get; init; }
|
||||||
|
|
||||||
|
public DateTime Expires { get; init; }
|
||||||
|
|
||||||
|
public string RefreshToken { get; init; }
|
||||||
|
|
||||||
|
public string Scope { get; init; }
|
||||||
|
}
|
||||||
23
VoidCat/Model/User/UserFlags.cs
Normal file
23
VoidCat/Model/User/UserFlags.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace VoidCat.Model.User;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Account status flags
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum UserFlags
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Profile is public
|
||||||
|
/// </summary>
|
||||||
|
PublicProfile = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uploads list is public
|
||||||
|
/// </summary>
|
||||||
|
PublicUploads = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Account has email verified
|
||||||
|
/// </summary>
|
||||||
|
EmailVerified = 4
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using VoidCat.Model.Payments;
|
using VoidCat.Model.Payments;
|
||||||
|
using VoidCat.Model.User;
|
||||||
|
|
||||||
namespace VoidCat.Model
|
namespace VoidCat.Model
|
||||||
{
|
{
|
||||||
@@ -24,7 +25,7 @@ namespace VoidCat.Model
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// User profile that uploaded the file
|
/// User profile that uploaded the file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PublicVoidUser? Uploader { get; init; }
|
public PublicUser? Uploader { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Traffic stats for this file
|
/// Traffic stats for this file
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ namespace VoidCat.Model
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class VoidSettings
|
public class VoidSettings
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base site url, used for redirect urls
|
||||||
|
/// </summary>
|
||||||
|
public Uri SiteUrl { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data directory to store files in
|
/// Data directory to store files in
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -15,7 +20,7 @@ namespace VoidCat.Model
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Size in bytes to split uploads into chunks
|
/// Size in bytes to split uploads into chunks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ulong? UploadSegmentSize { get; init; } = null;
|
public ulong? UploadSegmentSize { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tor configuration
|
/// Tor configuration
|
||||||
@@ -90,11 +95,16 @@ namespace VoidCat.Model
|
|||||||
/// Select which store to use for files storage, if not set "local-disk" will be used
|
/// Select which store to use for files storage, if not set "local-disk" will be used
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DefaultFileStore { get; init; } = "local-disk";
|
public string DefaultFileStore { get; init; } = "local-disk";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plausible Analytics endpoint url
|
/// Plausible Analytics endpoint url
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PlausibleSettings? PlausibleAnalytics { get; init; }
|
public PlausibleSettings? PlausibleAnalytics { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discord application settings
|
||||||
|
/// </summary>
|
||||||
|
public DiscordSettings? Discord { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TorSettings
|
public sealed class TorSettings
|
||||||
@@ -169,4 +179,10 @@ namespace VoidCat.Model
|
|||||||
public Uri? Endpoint { get; init; }
|
public Uri? Endpoint { get; init; }
|
||||||
public string? Domain { get; init; }
|
public string? Domain { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class DiscordSettings
|
||||||
|
{
|
||||||
|
public string? ClientId { get; init; }
|
||||||
|
public string? ClientSecret { get; init; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using VoidCat.Model;
|
using VoidCat.Model.User;
|
||||||
|
|
||||||
namespace VoidCat.Services.Abstractions;
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,7 @@ public interface IBasicStore<T>
|
|||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
ValueTask<T?> Get(Guid id);
|
ValueTask<T?> Get(Guid id);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get multiple items from the store
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ids"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
ValueTask<IReadOnlyList<T>> Get(Guid[] ids);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add an item to the store
|
/// Add an item to the store
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
|
|
||||||
namespace VoidCat.Services.Abstractions;
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
public interface IEmailVerification
|
public interface IEmailVerification
|
||||||
{
|
{
|
||||||
ValueTask<EmailVerificationCode> SendNewCode(PrivateVoidUser user);
|
ValueTask<EmailVerificationCode> SendNewCode(PrivateUser user);
|
||||||
|
|
||||||
ValueTask<bool> VerifyCode(PrivateVoidUser user, Guid code);
|
ValueTask<bool> VerifyCode(PrivateUser user, Guid code);
|
||||||
}
|
}
|
||||||
34
VoidCat/Services/Abstractions/IOAuthProvider.cs
Normal file
34
VoidCat/Services/Abstractions/IOAuthProvider.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using VoidCat.Model.User;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OAuth2 code grant provider
|
||||||
|
/// </summary>
|
||||||
|
public interface IOAuthProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id of this provider
|
||||||
|
/// </summary>
|
||||||
|
string Id { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate authorization code grant uri
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Uri Authorize();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get access token from auth code
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
ValueTask<UserAuthToken> GetToken(string code);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a user object which represents this external account authorization
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
ValueTask<InternalUser?> GetUserDetails(UserAuthToken token);
|
||||||
|
}
|
||||||
10
VoidCat/Services/Abstractions/IUserAuthTokenStore.cs
Normal file
10
VoidCat/Services/Abstractions/IUserAuthTokenStore.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using VoidCat.Model.User;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User access token store
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserAuthTokenStore : IBasicStore<UserAuthToken>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using VoidCat.Model;
|
|
||||||
|
|
||||||
namespace VoidCat.Services.Abstractions;
|
|
||||||
|
|
||||||
public interface IUserManager
|
|
||||||
{
|
|
||||||
ValueTask<InternalVoidUser> Login(string username, string password);
|
|
||||||
ValueTask<InternalVoidUser> Register(string username, string password);
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
|
|
||||||
namespace VoidCat.Services.Abstractions;
|
namespace VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// User store
|
/// User store
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IUserStore : IPublicPrivateStore<VoidUser, InternalVoidUser>
|
public interface IUserStore : IPublicPrivateStore<User, InternalUser>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a single user
|
/// Get a single user
|
||||||
@@ -13,7 +14,7 @@ public interface IUserStore : IPublicPrivateStore<VoidUser, InternalVoidUser>
|
|||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
/// <typeparam name="T"></typeparam>
|
/// <typeparam name="T"></typeparam>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
ValueTask<T?> Get<T>(Guid id) where T : VoidUser;
|
ValueTask<T?> Get<T>(Guid id) where T : User;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lookup a user by their email address
|
/// Lookup a user by their email address
|
||||||
@@ -27,14 +28,14 @@ public interface IUserStore : IPublicPrivateStore<VoidUser, InternalVoidUser>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request"></param>
|
/// <param name="request"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request);
|
ValueTask<PagedResult<PrivateUser>> ListUsers(PagedRequest request);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update a users profile
|
/// Update a users profile
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="newUser"></param>
|
/// <param name="newUser"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
ValueTask UpdateProfile(PublicVoidUser newUser);
|
ValueTask UpdateProfile(PublicUser newUser);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the last login timestamp for the user
|
/// Updates the last login timestamp for the user
|
||||||
@@ -49,5 +50,5 @@ public interface IUserStore : IPublicPrivateStore<VoidUser, InternalVoidUser>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
ValueTask AdminUpdateUser(PrivateVoidUser user);
|
ValueTask AdminUpdateUser(PrivateUser user);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
using VoidCat.Services.Files;
|
using VoidCat.Services.Files;
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ public class DeleteUnverifiedAccounts : BackgroundService
|
|||||||
|
|
||||||
await foreach (var account in accounts.Results.WithCancellation(stoppingToken))
|
await foreach (var account in accounts.Results.WithCancellation(stoppingToken))
|
||||||
{
|
{
|
||||||
if (!account.Flags.HasFlag(VoidUserFlags.EmailVerified) &&
|
if (!account.Flags.HasFlag(UserFlags.EmailVerified) &&
|
||||||
account.Created.AddDays(7) < DateTimeOffset.UtcNow)
|
account.Created.AddDays(7) < DateTimeOffset.UtcNow)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Deleting un-verified account: {Id}", account.Id.ToBase58());
|
_logger.LogInformation("Deleting un-verified account: {Id}", account.Id.ToBase58());
|
||||||
|
|||||||
@@ -5,45 +5,29 @@ namespace VoidCat.Services;
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract class BasicCacheStore<TStore> : IBasicStore<TStore>
|
public abstract class BasicCacheStore<TStore> : IBasicStore<TStore>
|
||||||
{
|
{
|
||||||
protected readonly ICache Cache;
|
protected readonly ICache _cache;
|
||||||
|
|
||||||
protected BasicCacheStore(ICache cache)
|
protected BasicCacheStore(ICache cache)
|
||||||
{
|
{
|
||||||
Cache = cache;
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual ValueTask<TStore?> Get(Guid id)
|
public virtual ValueTask<TStore?> Get(Guid id)
|
||||||
{
|
{
|
||||||
return Cache.Get<TStore>(MapKey(id));
|
return _cache.Get<TStore>(MapKey(id));
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public virtual async ValueTask<IReadOnlyList<TStore>> Get(Guid[] ids)
|
|
||||||
{
|
|
||||||
var ret = new List<TStore>();
|
|
||||||
foreach (var id in ids)
|
|
||||||
{
|
|
||||||
var r = await Cache.Get<TStore>(MapKey(id));
|
|
||||||
if (r != null)
|
|
||||||
{
|
|
||||||
ret.Add(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual ValueTask Add(Guid id, TStore obj)
|
public virtual ValueTask Add(Guid id, TStore obj)
|
||||||
{
|
{
|
||||||
return Cache.Set(MapKey(id), obj);
|
return _cache.Set(MapKey(id), obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual ValueTask Delete(Guid id)
|
public virtual ValueTask Delete(Guid id)
|
||||||
{
|
{
|
||||||
return Cache.Delete(MapKey(id));
|
return _cache.Delete(MapKey(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Services.Files;
|
namespace VoidCat.Services.Files;
|
||||||
@@ -91,7 +92,7 @@ public sealed class FileInfoManager
|
|||||||
await Task.WhenAll(meta.AsTask(), payment.AsTask(), bandwidth.AsTask(), virusScan.AsTask(), uploader.AsTask());
|
await Task.WhenAll(meta.AsTask(), payment.AsTask(), bandwidth.AsTask(), virusScan.AsTask(), uploader.AsTask());
|
||||||
|
|
||||||
if (meta.Result == default) return default;
|
if (meta.Result == default) return default;
|
||||||
var user = uploader.Result.HasValue ? await _userStore.Get<PublicVoidUser>(uploader.Result.Value) : null;
|
var user = uploader.Result.HasValue ? await _userStore.Get<PublicUser>(uploader.Result.Value) : null;
|
||||||
|
|
||||||
return new TFile()
|
return new TFile()
|
||||||
{
|
{
|
||||||
@@ -99,7 +100,7 @@ public sealed class FileInfoManager
|
|||||||
Metadata = meta.Result,
|
Metadata = meta.Result,
|
||||||
Payment = payment.Result,
|
Payment = payment.Result,
|
||||||
Bandwidth = bandwidth.Result,
|
Bandwidth = bandwidth.Result,
|
||||||
Uploader = user?.Flags.HasFlag(VoidUserFlags.PublicProfile) == true ? user : null,
|
Uploader = user?.Flags.HasFlag(UserFlags.PublicProfile) == true ? user : null,
|
||||||
VirusScan = virusScan.Result
|
VirusScan = virusScan.Result
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using FluentMigrator;
|
using FluentMigrator;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model.User;
|
||||||
|
|
||||||
namespace VoidCat.Services.Migrations.Database;
|
namespace VoidCat.Services.Migrations.Database;
|
||||||
|
|
||||||
@@ -11,13 +11,13 @@ public class Init : Migration
|
|||||||
{
|
{
|
||||||
Create.Table("Users")
|
Create.Table("Users")
|
||||||
.WithColumn("Id").AsGuid().PrimaryKey()
|
.WithColumn("Id").AsGuid().PrimaryKey()
|
||||||
.WithColumn("Email").AsString().NotNullable().Indexed()
|
.WithColumn("Email").AsString().Indexed()
|
||||||
.WithColumn("Password").AsString()
|
.WithColumn("Password").AsString()
|
||||||
.WithColumn("Created").AsDateTimeOffset().WithDefault(SystemMethods.CurrentUTCDateTime)
|
.WithColumn("Created").AsDateTimeOffset().WithDefault(SystemMethods.CurrentUTCDateTime)
|
||||||
.WithColumn("LastLogin").AsDateTimeOffset().Nullable()
|
.WithColumn("LastLogin").AsDateTimeOffset().Nullable()
|
||||||
.WithColumn("Avatar").AsString().Nullable()
|
.WithColumn("Avatar").AsString().Nullable()
|
||||||
.WithColumn("DisplayName").AsString().WithDefaultValue("void user")
|
.WithColumn("DisplayName").AsString().WithDefaultValue("void user")
|
||||||
.WithColumn("Flags").AsInt32().WithDefaultValue((int) VoidUserFlags.PublicProfile);
|
.WithColumn("Flags").AsInt32().WithDefaultValue((int) UserFlags.PublicProfile);
|
||||||
|
|
||||||
Create.Table("Files")
|
Create.Table("Files")
|
||||||
.WithColumn("Id").AsGuid().PrimaryKey()
|
.WithColumn("Id").AsGuid().PrimaryKey()
|
||||||
|
|||||||
44
VoidCat/Services/Migrations/Database/05-AccountTypes.cs
Normal file
44
VoidCat/Services/Migrations/Database/05-AccountTypes.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Data;
|
||||||
|
using FluentMigrator;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Migrations.Database;
|
||||||
|
|
||||||
|
[Migration(20220907_2015)]
|
||||||
|
public class AccountTypes : Migration
|
||||||
|
{
|
||||||
|
public override void Up()
|
||||||
|
{
|
||||||
|
Create.Column("AuthType")
|
||||||
|
.OnTable("Users")
|
||||||
|
.AsInt16()
|
||||||
|
.WithDefaultValue(0);
|
||||||
|
|
||||||
|
Alter.Column("Password")
|
||||||
|
.OnTable("Users")
|
||||||
|
.AsString()
|
||||||
|
.Nullable();
|
||||||
|
|
||||||
|
Create.Table("UsersAuthToken")
|
||||||
|
.WithColumn("Id").AsGuid().PrimaryKey()
|
||||||
|
.WithColumn("User").AsGuid().ForeignKey("Users", "Id").OnDelete(Rule.Cascade).Indexed()
|
||||||
|
.WithColumn("Provider").AsString()
|
||||||
|
.WithColumn("AccessToken").AsString()
|
||||||
|
.WithColumn("TokenType").AsString()
|
||||||
|
.WithColumn("Expires").AsDateTimeOffset()
|
||||||
|
.WithColumn("RefreshToken").AsString()
|
||||||
|
.WithColumn("Scope").AsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Down()
|
||||||
|
{
|
||||||
|
Delete.Column("Type")
|
||||||
|
.FromTable("Users");
|
||||||
|
|
||||||
|
Alter.Column("Password")
|
||||||
|
.OnTable("Users")
|
||||||
|
.AsString()
|
||||||
|
.NotNullable();
|
||||||
|
|
||||||
|
Delete.Table("UsersAuthToken");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
using VoidCat.Services.Files;
|
using VoidCat.Services.Files;
|
||||||
using VoidCat.Services.Payment;
|
using VoidCat.Services.Payment;
|
||||||
@@ -132,7 +133,7 @@ public class MigrateToPostgres : IMigration
|
|||||||
var privateUser = await cacheUsers.Get<PrivateUser>(user.Id);
|
var privateUser = await cacheUsers.Get<PrivateUser>(user.Id);
|
||||||
privateUser!.Password ??= privateUser.PasswordHash;
|
privateUser!.Password ??= privateUser.PasswordHash;
|
||||||
|
|
||||||
await _userStore.Set(privateUser!.Id, new InternalVoidUser()
|
await _userStore.Set(privateUser!.Id, new InternalUser()
|
||||||
{
|
{
|
||||||
Id = privateUser.Id,
|
Id = privateUser.Id,
|
||||||
Avatar = privateUser.Avatar,
|
Avatar = privateUser.Avatar,
|
||||||
@@ -154,7 +155,7 @@ public class MigrateToPostgres : IMigration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PrivateUser : PrivateVoidUser
|
private class PrivateUser : Model.User.PrivateUser
|
||||||
{
|
{
|
||||||
public string? PasswordHash { get; set; }
|
public string? PasswordHash { get; set; }
|
||||||
public string? Password { get; set; }
|
public string? Password { get; set; }
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ public class CachePaymentStore : BasicCacheStore<PaymentConfig>, IPaymentStore
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async ValueTask<PaymentConfig?> Get(Guid id)
|
public override async ValueTask<PaymentConfig?> Get(Guid id)
|
||||||
{
|
{
|
||||||
var cfg = await Cache.Get<NoPaymentConfig>(MapKey(id));
|
var cfg = await _cache.Get<NoPaymentConfig>(MapKey(id));
|
||||||
return cfg?.Service switch
|
return cfg?.Service switch
|
||||||
{
|
{
|
||||||
PaymentServices.None => cfg,
|
PaymentServices.None => cfg,
|
||||||
PaymentServices.Strike => await Cache.Get<StrikePaymentConfig>(MapKey(id)),
|
PaymentServices.Strike => await _cache.Get<StrikePaymentConfig>(MapKey(id)),
|
||||||
_ => default
|
_ => default
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
15
VoidCat/Services/Users/Auth/CacheUserAuthTokenStore.cs
Normal file
15
VoidCat/Services/Users/Auth/CacheUserAuthTokenStore.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using VoidCat.Model.User;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Users.Auth;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IUserAuthTokenStore"/>
|
||||||
|
public class CacheUserAuthTokenStore : BasicCacheStore<UserAuthToken>, IUserAuthTokenStore
|
||||||
|
{
|
||||||
|
public CacheUserAuthTokenStore(ICache cache) : base(cache)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override string MapKey(Guid id) => $"auth-token:{id}";
|
||||||
|
}
|
||||||
118
VoidCat/Services/Users/Auth/DiscordOAuthProvider.cs
Normal file
118
VoidCat/Services/Users/Auth/DiscordOAuthProvider.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Users.Auth;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class DiscordOAuthProvider : GenericOAuth2Service<DiscordAccessToken>
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly DiscordSettings _discord;
|
||||||
|
private readonly Uri _site;
|
||||||
|
|
||||||
|
public DiscordOAuthProvider(HttpClient client, VoidSettings settings) : base(client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_discord = settings.Discord!;
|
||||||
|
_site = settings.SiteUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Id => "discord";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async ValueTask<InternalUser?> GetUserDetails(UserAuthToken token)
|
||||||
|
{
|
||||||
|
var req = new HttpRequestMessage(HttpMethod.Get, "https://discord.com/api/users/@me");
|
||||||
|
req.Headers.Authorization = new("Bearer", token.AccessToken);
|
||||||
|
|
||||||
|
var rsp = await _client.SendAsync(req);
|
||||||
|
if (rsp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var user = JsonConvert.DeserializeObject<DiscordUser>(await rsp.Content.ReadAsStringAsync());
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
AuthType = AuthType.OAuth2,
|
||||||
|
DisplayName = $"{user!.Username}",
|
||||||
|
Avatar = !string.IsNullOrEmpty(user.Avatar)
|
||||||
|
? $"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}.png"
|
||||||
|
: null,
|
||||||
|
Email = user.Email!,
|
||||||
|
Created = DateTimeOffset.UtcNow,
|
||||||
|
LastLogin = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Dictionary<string, string> BuildAuthorizeQuery()
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
{"response_type", "code"},
|
||||||
|
{"client_id", _discord.ClientId!},
|
||||||
|
{"scope", "email identify"},
|
||||||
|
{"prompt", "none"},
|
||||||
|
{"redirect_uri", new Uri(_site, $"/auth/{Id}/token").ToString()}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override Dictionary<string, string> BuildTokenQuery(string code)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
{"client_id", _discord.ClientId!},
|
||||||
|
{"client_secret", _discord.ClientSecret!},
|
||||||
|
{"grant_type", "authorization_code"},
|
||||||
|
{"code", code},
|
||||||
|
{"redirect_uri", new Uri(_site, $"/auth/{Id}/token").ToString()}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override UserAuthToken TransformDto(DiscordAccessToken dto)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Provider = Id,
|
||||||
|
AccessToken = dto.AccessToken,
|
||||||
|
Expires = DateTime.UtcNow.AddSeconds(dto.ExpiresIn),
|
||||||
|
TokenType = dto.TokenType,
|
||||||
|
RefreshToken = dto.RefreshToken,
|
||||||
|
Scope = dto.Scope
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Uri AuthorizeEndpoint => new("https://discord.com/oauth2/authorize");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Uri TokenEndpoint => new("https://discord.com/api/oauth2/token");
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DiscordAccessToken
|
||||||
|
{
|
||||||
|
[JsonProperty("access_token")] public string AccessToken { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("expires_in")] public int ExpiresIn { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("token_type")] public string TokenType { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("refresh_token")] public string RefreshToken { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("scope")] public string Scope { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DiscordUser
|
||||||
|
{
|
||||||
|
[JsonProperty("id")] public string Id { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty("username")] public string Username { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty("discriminator")] public string Discriminator { get; init; } = null!;
|
||||||
|
|
||||||
|
[JsonProperty("avatar")] public string? Avatar { get; init; }
|
||||||
|
|
||||||
|
[JsonProperty("email")] public string? Email { get; init; }
|
||||||
|
}
|
||||||
78
VoidCat/Services/Users/Auth/GenericOAuth2Service.cs
Normal file
78
VoidCat/Services/Users/Auth/GenericOAuth2Service.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using VoidCat.Model.User;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Users.Auth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generic base class for OAuth2 code grant flow
|
||||||
|
/// </summary>
|
||||||
|
public abstract class GenericOAuth2Service<TDto> : IOAuthProvider
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
|
||||||
|
protected GenericOAuth2Service(HttpClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract string Id { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Uri Authorize()
|
||||||
|
{
|
||||||
|
var ub = new UriBuilder(AuthorizeEndpoint)
|
||||||
|
{
|
||||||
|
Query = string.Join("&", BuildAuthorizeQuery().Select(a => $"{a.Key}={Uri.EscapeDataString(a.Value)}"))
|
||||||
|
};
|
||||||
|
|
||||||
|
return ub.Uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask<UserAuthToken> GetToken(string code)
|
||||||
|
{
|
||||||
|
var form = new FormUrlEncodedContent(BuildTokenQuery(code));
|
||||||
|
var rsp = await _client.PostAsync(TokenEndpoint, form);
|
||||||
|
var json = await rsp.Content.ReadAsStringAsync();
|
||||||
|
if (!rsp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to get token from provider: {Id}, response: {json}");
|
||||||
|
}
|
||||||
|
var dto = JsonConvert.DeserializeObject<TDto>(json);
|
||||||
|
return TransformDto(dto!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract ValueTask<InternalUser?> GetUserDetails(UserAuthToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build query args for authorize
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected abstract Dictionary<string, string> BuildAuthorizeQuery();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build query args for token generation
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected abstract Dictionary<string, string> BuildTokenQuery(string code);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transform DTO to <see cref="UserAuthToken"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected abstract UserAuthToken TransformDto(TDto dto);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authorize url for this service
|
||||||
|
/// </summary>
|
||||||
|
protected abstract Uri AuthorizeEndpoint { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate token url for this service
|
||||||
|
/// </summary>
|
||||||
|
protected abstract Uri TokenEndpoint { get; }
|
||||||
|
}
|
||||||
33
VoidCat/Services/Users/Auth/OAuthFactory.cs
Normal file
33
VoidCat/Services/Users/Auth/OAuthFactory.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Users.Auth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory class to access specific OAuth providers
|
||||||
|
/// </summary>
|
||||||
|
public sealed class OAuthFactory
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<IOAuthProvider> _providers;
|
||||||
|
|
||||||
|
public OAuthFactory(IEnumerable<IOAuthProvider> providers)
|
||||||
|
{
|
||||||
|
_providers = providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get an OAuth provider by id
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public IOAuthProvider GetProvider(string id)
|
||||||
|
{
|
||||||
|
var provider = _providers.FirstOrDefault(a => a.Id.Equals(id, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
if (provider == default)
|
||||||
|
{
|
||||||
|
throw new Exception($"OAuth provider not found: {id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
VoidCat/Services/Users/Auth/PostgresUserAuthTokenStore.cs
Normal file
56
VoidCat/Services/Users/Auth/PostgresUserAuthTokenStore.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Dapper;
|
||||||
|
using VoidCat.Model.User;
|
||||||
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services.Users.Auth;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class PostgresUserAuthTokenStore : IUserAuthTokenStore
|
||||||
|
{
|
||||||
|
private readonly PostgresConnectionFactory _connection;
|
||||||
|
|
||||||
|
public PostgresUserAuthTokenStore(PostgresConnectionFactory connection)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask<UserAuthToken?> Get(Guid id)
|
||||||
|
{
|
||||||
|
await using var conn = await _connection.Get();
|
||||||
|
return await conn.QuerySingleOrDefaultAsync<UserAuthToken>(
|
||||||
|
@"select * from ""UsersAuthToken"" where ""User"" = :id", new {id});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask Add(Guid id, UserAuthToken obj)
|
||||||
|
{
|
||||||
|
await using var conn = await _connection.Get();
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
@"insert into ""UsersAuthToken""(""Id"", ""User"", ""Provider"", ""AccessToken"", ""TokenType"", ""Expires"", ""RefreshToken"", ""Scope"")
|
||||||
|
values(:id, :user, :provider, :accessToken, :tokenType, :expires, :refreshToken, :scope)
|
||||||
|
on conflict(""Id"") do update set
|
||||||
|
""AccessToken"" = :accessToken,
|
||||||
|
""TokenType"" = :tokenType,
|
||||||
|
""Expires"" = :expires,
|
||||||
|
""RefreshToken"" = :refreshToken,
|
||||||
|
""Scope"" = :scope", new
|
||||||
|
{
|
||||||
|
id = obj.Id,
|
||||||
|
user = obj.User,
|
||||||
|
provider = obj.Provider,
|
||||||
|
accessToken = obj.AccessToken,
|
||||||
|
tokenType = obj.TokenType,
|
||||||
|
expires = obj.Expires.ToUniversalTime(),
|
||||||
|
refreshToken = obj.RefreshToken,
|
||||||
|
scope = obj.Scope
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask Delete(Guid id)
|
||||||
|
{
|
||||||
|
await using var conn = await _connection.Get();
|
||||||
|
await conn.ExecuteAsync(@"delete from ""UsersAuthToken"" where ""Id"" = :id", new {id});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mail;
|
using System.Net.Mail;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Services.Users;
|
namespace VoidCat.Services.Users;
|
||||||
@@ -22,7 +23,7 @@ public abstract class BaseEmailVerification : IEmailVerification
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask<EmailVerificationCode> SendNewCode(PrivateVoidUser user)
|
public async ValueTask<EmailVerificationCode> SendNewCode(PrivateUser user)
|
||||||
{
|
{
|
||||||
var token = new EmailVerificationCode(user.Id, Guid.NewGuid(), DateTime.UtcNow.AddHours(HoursExpire));
|
var token = new EmailVerificationCode(user.Id, Guid.NewGuid(), DateTime.UtcNow.AddHours(HoursExpire));
|
||||||
await SaveToken(token);
|
await SaveToken(token);
|
||||||
@@ -59,7 +60,7 @@ public abstract class BaseEmailVerification : IEmailVerification
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask<bool> VerifyCode(PrivateVoidUser user, Guid code)
|
public async ValueTask<bool> VerifyCode(PrivateUser user, Guid code)
|
||||||
{
|
{
|
||||||
var token = await GetToken(user.Id, code);
|
var token = await GetToken(user.Id, code);
|
||||||
if (token == default) return false;
|
if (token == default) return false;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using VoidCat.Model;
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Services.Users;
|
namespace VoidCat.Services.Users;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Services.Users;
|
namespace VoidCat.Services.Users;
|
||||||
@@ -21,25 +22,25 @@ public class CacheUserStore : IUserStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ValueTask<T?> Get<T>(Guid id) where T : VoidUser
|
public ValueTask<T?> Get<T>(Guid id) where T : User
|
||||||
{
|
{
|
||||||
return _cache.Get<T>(MapKey(id));
|
return _cache.Get<T>(MapKey(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ValueTask<VoidUser?> Get(Guid id)
|
public ValueTask<User?> Get(Guid id)
|
||||||
{
|
{
|
||||||
return Get<VoidUser>(id);
|
return Get<User>(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ValueTask<InternalVoidUser?> GetPrivate(Guid id)
|
public ValueTask<InternalUser?> GetPrivate(Guid id)
|
||||||
{
|
{
|
||||||
return Get<InternalVoidUser>(id);
|
return Get<InternalUser>(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask Set(Guid id, InternalVoidUser user)
|
public async ValueTask Set(Guid id, InternalUser user)
|
||||||
{
|
{
|
||||||
if (id != user.Id) throw new InvalidOperationException();
|
if (id != user.Id) throw new InvalidOperationException();
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ public class CacheUserStore : IUserStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request)
|
public async ValueTask<PagedResult<PrivateUser>> ListUsers(PagedRequest request)
|
||||||
{
|
{
|
||||||
var users = (await _cache.GetList(UserList))
|
var users = (await _cache.GetList(UserList))
|
||||||
.Select<string, Guid?>(a => Guid.TryParse(a, out var g) ? g : null)
|
.Select<string, Guid?>(a => Guid.TryParse(a, out var g) ? g : null)
|
||||||
@@ -61,9 +62,9 @@ public class CacheUserStore : IUserStore
|
|||||||
_ => users
|
_ => users
|
||||||
};
|
};
|
||||||
|
|
||||||
async IAsyncEnumerable<PrivateVoidUser> EnumerateUsers(IEnumerable<Guid> ids)
|
async IAsyncEnumerable<PrivateUser> EnumerateUsers(IEnumerable<Guid> ids)
|
||||||
{
|
{
|
||||||
var usersLoaded = await Task.WhenAll(ids.Select(async a => await Get<PrivateVoidUser>(a)));
|
var usersLoaded = await Task.WhenAll(ids.Select(async a => await Get<PrivateUser>(a)));
|
||||||
foreach (var user in usersLoaded)
|
foreach (var user in usersLoaded)
|
||||||
{
|
{
|
||||||
if (user != default)
|
if (user != default)
|
||||||
@@ -83,17 +84,17 @@ public class CacheUserStore : IUserStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask UpdateProfile(PublicVoidUser newUser)
|
public async ValueTask UpdateProfile(PublicUser newUser)
|
||||||
{
|
{
|
||||||
var oldUser = await Get<InternalVoidUser>(newUser.Id);
|
var oldUser = await Get<InternalUser>(newUser.Id);
|
||||||
if (oldUser == null) return;
|
if (oldUser == null) return;
|
||||||
|
|
||||||
//retain flags
|
//retain flags
|
||||||
var isEmailVerified = oldUser.Flags.HasFlag(VoidUserFlags.EmailVerified);
|
var isEmailVerified = oldUser.Flags.HasFlag(UserFlags.EmailVerified);
|
||||||
|
|
||||||
// update only a few props
|
// update only a few props
|
||||||
oldUser.Avatar = newUser.Avatar;
|
oldUser.Avatar = newUser.Avatar;
|
||||||
oldUser.Flags = newUser.Flags | (isEmailVerified ? VoidUserFlags.EmailVerified : 0);
|
oldUser.Flags = newUser.Flags | (isEmailVerified ? UserFlags.EmailVerified : 0);
|
||||||
oldUser.DisplayName = newUser.DisplayName;
|
oldUser.DisplayName = newUser.DisplayName;
|
||||||
|
|
||||||
await Set(newUser.Id, oldUser);
|
await Set(newUser.Id, oldUser);
|
||||||
@@ -102,7 +103,7 @@ public class CacheUserStore : IUserStore
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask UpdateLastLogin(Guid id, DateTime timestamp)
|
public async ValueTask UpdateLastLogin(Guid id, DateTime timestamp)
|
||||||
{
|
{
|
||||||
var user = await Get<InternalVoidUser>(id);
|
var user = await Get<InternalUser>(id);
|
||||||
if (user != default)
|
if (user != default)
|
||||||
{
|
{
|
||||||
user.LastLogin = timestamp;
|
user.LastLogin = timestamp;
|
||||||
@@ -111,9 +112,9 @@ public class CacheUserStore : IUserStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask AdminUpdateUser(PrivateVoidUser user)
|
public async ValueTask AdminUpdateUser(PrivateUser user)
|
||||||
{
|
{
|
||||||
var oldUser = await Get<InternalVoidUser>(user.Id);
|
var oldUser = await Get<InternalUser>(user.Id);
|
||||||
if (oldUser == null) return;
|
if (oldUser == null) return;
|
||||||
|
|
||||||
oldUser.Email = user.Email;
|
oldUser.Email = user.Email;
|
||||||
@@ -125,12 +126,12 @@ public class CacheUserStore : IUserStore
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask Delete(Guid id)
|
public async ValueTask Delete(Guid id)
|
||||||
{
|
{
|
||||||
var user = await Get<InternalVoidUser>(id);
|
var user = await Get<InternalUser>(id);
|
||||||
if (user == default) throw new InvalidOperationException();
|
if (user == default) throw new InvalidOperationException();
|
||||||
await Delete(user);
|
await Delete(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask Delete(PrivateVoidUser user)
|
private async ValueTask Delete(PrivateUser user)
|
||||||
{
|
{
|
||||||
await _cache.Delete(MapKey(user.Id));
|
await _cache.Delete(MapKey(user.Id));
|
||||||
await _cache.RemoveFromList(UserList, user.Id.ToString());
|
await _cache.RemoveFromList(UserList, user.Id.ToString());
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Services.Users;
|
namespace VoidCat.Services.Users;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
|
||||||
namespace VoidCat.Services.Users;
|
namespace VoidCat.Services.Users;
|
||||||
@@ -15,25 +16,25 @@ public class PostgresUserStore : IUserStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask<VoidUser?> Get(Guid id)
|
public async ValueTask<User?> Get(Guid id)
|
||||||
{
|
{
|
||||||
return await Get<PublicVoidUser>(id);
|
return await Get<PublicUser>(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask<InternalVoidUser?> GetPrivate(Guid id)
|
public async ValueTask<InternalUser?> GetPrivate(Guid id)
|
||||||
{
|
{
|
||||||
return await Get<InternalVoidUser>(id);
|
return await Get<InternalUser>(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask Set(Guid id, InternalVoidUser obj)
|
public async ValueTask Set(Guid id, InternalUser obj)
|
||||||
{
|
{
|
||||||
await using var conn = await _connection.Get();
|
await using var conn = await _connection.Get();
|
||||||
await conn.ExecuteAsync(
|
await conn.ExecuteAsync(
|
||||||
@"insert into
|
@"insert into
|
||||||
""Users""(""Id"", ""Email"", ""Password"", ""Created"", ""LastLogin"", ""DisplayName"", ""Avatar"", ""Flags"")
|
""Users""(""Id"", ""Email"", ""Password"", ""Created"", ""LastLogin"", ""DisplayName"", ""Avatar"", ""Flags"", ""AuthType"")
|
||||||
values(:id, :email, :password, :created, :lastLogin, :displayName, :avatar, :flags)",
|
values(:id, :email, :password, :created, :lastLogin, :displayName, :avatar, :flags, :authType)",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
@@ -43,7 +44,8 @@ values(:id, :email, :password, :created, :lastLogin, :displayName, :avatar, :fla
|
|||||||
displayName = obj.DisplayName,
|
displayName = obj.DisplayName,
|
||||||
lastLogin = obj.LastLogin.ToUniversalTime(),
|
lastLogin = obj.LastLogin.ToUniversalTime(),
|
||||||
avatar = obj.Avatar,
|
avatar = obj.Avatar,
|
||||||
flags = (int)obj.Flags
|
flags = (int)obj.Flags,
|
||||||
|
authType = (int)obj.AuthType
|
||||||
});
|
});
|
||||||
|
|
||||||
if (obj.Roles.Any(a => a != Roles.User))
|
if (obj.Roles.Any(a => a != Roles.User))
|
||||||
@@ -65,7 +67,7 @@ values(:id, :email, :password, :created, :lastLogin, :displayName, :avatar, :fla
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask<T?> Get<T>(Guid id) where T : VoidUser
|
public async ValueTask<T?> Get<T>(Guid id) where T : User
|
||||||
{
|
{
|
||||||
await using var conn = await _connection.Get();
|
await using var conn = await _connection.Get();
|
||||||
var user = await conn.QuerySingleOrDefaultAsync<T?>(@"select * from ""Users"" where ""Id"" = :id",
|
var user = await conn.QuerySingleOrDefaultAsync<T?>(@"select * from ""Users"" where ""Id"" = :id",
|
||||||
@@ -96,12 +98,12 @@ values(:id, :email, :password, :created, :lastLogin, :displayName, :avatar, :fla
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask<PagedResult<PrivateVoidUser>> ListUsers(PagedRequest request)
|
public async ValueTask<PagedResult<PrivateUser>> ListUsers(PagedRequest request)
|
||||||
{
|
{
|
||||||
await using var conn = await _connection.Get();
|
await using var conn = await _connection.Get();
|
||||||
var totalUsers = await conn.ExecuteScalarAsync<int>(@"select count(*) from ""Users""");
|
var totalUsers = await conn.ExecuteScalarAsync<int>(@"select count(*) from ""Users""");
|
||||||
|
|
||||||
async IAsyncEnumerable<PrivateVoidUser> Enumerate()
|
async IAsyncEnumerable<PrivateUser> Enumerate()
|
||||||
{
|
{
|
||||||
var orderBy = request.SortBy switch
|
var orderBy = request.SortBy switch
|
||||||
{
|
{
|
||||||
@@ -125,7 +127,7 @@ values(:id, :email, :password, :created, :lastLogin, :displayName, :avatar, :fla
|
|||||||
limit = request.PageSize
|
limit = request.PageSize
|
||||||
});
|
});
|
||||||
|
|
||||||
var rowParser = users.GetRowParser<PrivateVoidUser>();
|
var rowParser = users.GetRowParser<PrivateUser>();
|
||||||
while (await users.ReadAsync())
|
while (await users.ReadAsync())
|
||||||
{
|
{
|
||||||
yield return rowParser(users);
|
yield return rowParser(users);
|
||||||
@@ -142,12 +144,12 @@ values(:id, :email, :password, :created, :lastLogin, :displayName, :avatar, :fla
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask UpdateProfile(PublicVoidUser newUser)
|
public async ValueTask UpdateProfile(PublicUser newUser)
|
||||||
{
|
{
|
||||||
var oldUser = await Get<InternalVoidUser>(newUser.Id);
|
var oldUser = await Get<InternalUser>(newUser.Id);
|
||||||
if (oldUser == null) return;
|
if (oldUser == null) return;
|
||||||
|
|
||||||
var emailFlag = oldUser.Flags.HasFlag(VoidUserFlags.EmailVerified) ? VoidUserFlags.EmailVerified : 0;
|
var emailFlag = oldUser.Flags.HasFlag(UserFlags.EmailVerified) ? UserFlags.EmailVerified : 0;
|
||||||
await using var conn = await _connection.Get();
|
await using var conn = await _connection.Get();
|
||||||
await conn.ExecuteAsync(
|
await conn.ExecuteAsync(
|
||||||
@"update ""Users"" set ""DisplayName"" = :displayName, ""Avatar"" = :avatar, ""Flags"" = :flags where ""Id"" = :id",
|
@"update ""Users"" set ""DisplayName"" = :displayName, ""Avatar"" = :avatar, ""Flags"" = :flags where ""Id"" = :id",
|
||||||
@@ -169,7 +171,7 @@ values(:id, :email, :password, :created, :lastLogin, :displayName, :avatar, :fla
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask AdminUpdateUser(PrivateVoidUser user)
|
public async ValueTask AdminUpdateUser(PrivateUser user)
|
||||||
{
|
{
|
||||||
await using var conn = await _connection.Get();
|
await using var conn = await _connection.Get();
|
||||||
await conn.ExecuteAsync(
|
await conn.ExecuteAsync(
|
||||||
|
|||||||
@@ -1,44 +1,60 @@
|
|||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.User;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
using VoidCat.Services.Users.Auth;
|
||||||
|
|
||||||
namespace VoidCat.Services.Users;
|
namespace VoidCat.Services.Users;
|
||||||
|
|
||||||
/// <inheritdoc />
|
public class UserManager
|
||||||
public class UserManager : IUserManager
|
|
||||||
{
|
{
|
||||||
private readonly IUserStore _store;
|
private readonly IUserStore _store;
|
||||||
private readonly IEmailVerification _emailVerification;
|
private readonly IEmailVerification _emailVerification;
|
||||||
|
private readonly IUserAuthTokenStore _tokenStore;
|
||||||
|
private readonly OAuthFactory _oAuthFactory;
|
||||||
private static bool _checkFirstRegister;
|
private static bool _checkFirstRegister;
|
||||||
|
|
||||||
public UserManager(IUserStore store, IEmailVerification emailVerification)
|
public UserManager(IUserStore store, IEmailVerification emailVerification, OAuthFactory oAuthFactory,
|
||||||
|
IUserAuthTokenStore tokenStore)
|
||||||
{
|
{
|
||||||
_store = store;
|
_store = store;
|
||||||
_emailVerification = emailVerification;
|
_emailVerification = emailVerification;
|
||||||
|
_oAuthFactory = oAuthFactory;
|
||||||
|
_tokenStore = tokenStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public async ValueTask<InternalVoidUser> Login(string email, string password)
|
/// Login an existing user with email/password
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="email"></param>
|
||||||
|
/// <param name="password"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public async ValueTask<InternalUser> Login(string email, string password)
|
||||||
{
|
{
|
||||||
var userId = await _store.LookupUser(email);
|
var userId = await _store.LookupUser(email);
|
||||||
if (!userId.HasValue) throw new InvalidOperationException("User does not exist");
|
if (!userId.HasValue) throw new InvalidOperationException("User does not exist");
|
||||||
|
|
||||||
var user = await _store.GetPrivate(userId.Value);
|
var user = await _store.GetPrivate(userId.Value);
|
||||||
if (!(user?.CheckPassword(password) ?? false)) throw new InvalidOperationException("User does not exist");
|
if (!(user?.CheckPassword(password) ?? false)) throw new InvalidOperationException("User does not exist");
|
||||||
|
|
||||||
user.LastLogin = DateTimeOffset.UtcNow;
|
|
||||||
await _store.UpdateLastLogin(user.Id, DateTime.UtcNow);
|
|
||||||
|
|
||||||
|
await HandleLogin(user);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public async ValueTask<InternalVoidUser> Register(string email, string password)
|
/// Register a new internal user with email/password
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="email"></param>
|
||||||
|
/// <param name="password"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public async ValueTask<InternalUser> Register(string email, string password)
|
||||||
{
|
{
|
||||||
var existingUser = await _store.LookupUser(email);
|
var existingUser = await _store.LookupUser(email);
|
||||||
if (existingUser != Guid.Empty && existingUser != null)
|
if (existingUser != Guid.Empty && existingUser != null)
|
||||||
throw new InvalidOperationException("User already exists");
|
throw new InvalidOperationException("User already exists");
|
||||||
|
|
||||||
var newUser = new InternalVoidUser
|
var newUser = new InternalUser
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Email = email,
|
Email = email,
|
||||||
@@ -47,6 +63,56 @@ public class UserManager : IUserManager
|
|||||||
LastLogin = DateTimeOffset.UtcNow
|
LastLogin = DateTimeOffset.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await SetupNewUser(newUser);
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start OAuth2 authorization flow
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Uri Authorize(string provider)
|
||||||
|
{
|
||||||
|
var px = _oAuthFactory.GetProvider(provider);
|
||||||
|
return px.Authorize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Login or Register with OAuth2 auth code
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="provider"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async ValueTask<InternalUser> LoginOrRegister(string code, string provider)
|
||||||
|
{
|
||||||
|
var px = _oAuthFactory.GetProvider(provider);
|
||||||
|
var token = await px.GetToken(code);
|
||||||
|
|
||||||
|
var user = await px.GetUserDetails(token);
|
||||||
|
if (user == default)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Could not load user profile from provider: {provider}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var uid = await _store.LookupUser(user.Email);
|
||||||
|
if (uid.HasValue)
|
||||||
|
{
|
||||||
|
var existingUser = await _store.GetPrivate(uid.Value);
|
||||||
|
if (existingUser?.AuthType == AuthType.OAuth2)
|
||||||
|
{
|
||||||
|
return existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Auth failure, user type does not match!");
|
||||||
|
}
|
||||||
|
|
||||||
|
await SetupNewUser(user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetupNewUser(InternalUser newUser)
|
||||||
|
{
|
||||||
// automatically set first user to admin
|
// automatically set first user to admin
|
||||||
if (!_checkFirstRegister)
|
if (!_checkFirstRegister)
|
||||||
{
|
{
|
||||||
@@ -60,6 +126,11 @@ public class UserManager : IUserManager
|
|||||||
|
|
||||||
await _store.Set(newUser.Id, newUser);
|
await _store.Set(newUser.Id, newUser);
|
||||||
await _emailVerification.SendNewCode(newUser);
|
await _emailVerification.SendNewCode(newUser);
|
||||||
return newUser;
|
}
|
||||||
|
|
||||||
|
private async Task HandleLogin(InternalUser user)
|
||||||
|
{
|
||||||
|
user.LastLogin = DateTimeOffset.UtcNow;
|
||||||
|
await _store.UpdateLastLogin(user.Id, DateTime.UtcNow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
using VoidCat.Services.Abstractions;
|
using VoidCat.Services.Abstractions;
|
||||||
|
using VoidCat.Services.Users.Auth;
|
||||||
|
|
||||||
namespace VoidCat.Services.Users;
|
namespace VoidCat.Services.Users;
|
||||||
|
|
||||||
@@ -7,19 +8,27 @@ public static class UsersStartup
|
|||||||
{
|
{
|
||||||
public static void AddUserServices(this IServiceCollection services, VoidSettings settings)
|
public static void AddUserServices(this IServiceCollection services, VoidSettings settings)
|
||||||
{
|
{
|
||||||
services.AddTransient<IUserManager, UserManager>();
|
services.AddTransient<UserManager>();
|
||||||
|
services.AddTransient<OAuthFactory>();
|
||||||
|
|
||||||
|
if (settings.HasDiscord())
|
||||||
|
{
|
||||||
|
services.AddTransient<IOAuthProvider, DiscordOAuthProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
if (settings.HasPostgres())
|
if (settings.HasPostgres())
|
||||||
{
|
{
|
||||||
services.AddTransient<IUserStore, PostgresUserStore>();
|
services.AddTransient<IUserStore, PostgresUserStore>();
|
||||||
services.AddTransient<IEmailVerification, PostgresEmailVerification>();
|
services.AddTransient<IEmailVerification, PostgresEmailVerification>();
|
||||||
services.AddTransient<IApiKeyStore, PostgresApiKeyStore>();
|
services.AddTransient<IApiKeyStore, PostgresApiKeyStore>();
|
||||||
|
services.AddTransient<IUserAuthTokenStore, PostgresUserAuthTokenStore>();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
services.AddTransient<IUserStore, CacheUserStore>();
|
services.AddTransient<IUserStore, CacheUserStore>();
|
||||||
services.AddTransient<IEmailVerification, CacheEmailVerification>();
|
services.AddTransient<IEmailVerification, CacheEmailVerification>();
|
||||||
services.AddTransient<IApiKeyStore, CacheApiKeyStore>();
|
services.AddTransient<IApiKeyStore, CacheApiKeyStore>();
|
||||||
|
services.AddTransient<IUserAuthTokenStore, CacheUserAuthTokenStore>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,13 +14,13 @@ public class CacheVirusScanStore : BasicCacheStore<VirusScanResult>, IVirusScanS
|
|||||||
public override async ValueTask Add(Guid id, VirusScanResult obj)
|
public override async ValueTask Add(Guid id, VirusScanResult obj)
|
||||||
{
|
{
|
||||||
await base.Add(id, obj);
|
await base.Add(id, obj);
|
||||||
await Cache.AddToList(MapFilesKey(id), obj.Id.ToString());
|
await _cache.AddToList(MapFilesKey(id), obj.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask<VirusScanResult?> GetByFile(Guid id)
|
public async ValueTask<VirusScanResult?> GetByFile(Guid id)
|
||||||
{
|
{
|
||||||
var scans = await Cache.GetList(MapFilesKey(id));
|
var scans = await _cache.GetList(MapFilesKey(id));
|
||||||
if (scans.Length > 0)
|
if (scans.Length > 0)
|
||||||
{
|
{
|
||||||
return await Get(Guid.Parse(scans.First()));
|
return await Get(Guid.Parse(scans.First()));
|
||||||
|
|||||||
@@ -30,14 +30,6 @@ public class PostgresVirusScanStore : IVirusScanStore
|
|||||||
@"select * from ""VirusScanResult"" where ""File"" = :file", new {file = id});
|
@"select * from ""VirusScanResult"" where ""File"" = :file", new {file = id});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async ValueTask<IReadOnlyList<VirusScanResult>> Get(Guid[] ids)
|
|
||||||
{
|
|
||||||
await using var conn = await _connection.Get();
|
|
||||||
return (await conn.QueryAsync<VirusScanResult>(
|
|
||||||
@"select * from ""VirusScanResult"" where ""Id"" in :ids", new {ids = ids.ToArray()})).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask Add(Guid id, VirusScanResult obj)
|
public async ValueTask Add(Guid id, VirusScanResult obj)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export function Login() {
|
|||||||
{captchaKey ? <HCaptcha sitekey={captchaKey} onVerify={setCaptchaResponse}/> : null}
|
{captchaKey ? <HCaptcha sitekey={captchaKey} onVerify={setCaptchaResponse}/> : null}
|
||||||
<VoidButton onClick={() => login(Api.login)}>Login</VoidButton>
|
<VoidButton onClick={() => login(Api.login)}>Login</VoidButton>
|
||||||
<VoidButton onClick={() => login(Api.register)}>Register</VoidButton>
|
<VoidButton onClick={() => login(Api.register)}>Register</VoidButton>
|
||||||
|
<br/>
|
||||||
|
<VoidButton onClick={() => window.location.href = `/auth/discord`}>Login with Discord</VoidButton>
|
||||||
{error ? <div className="error-msg">{error}</div> : null}
|
{error ? <div className="error-msg">{error}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ const LocalStorageKey = "token";
|
|||||||
export const LoginState = createSlice({
|
export const LoginState = createSlice({
|
||||||
name: "Login",
|
name: "Login",
|
||||||
initialState: {
|
initialState: {
|
||||||
jwt: window.localStorage.getItem(LocalStorageKey),
|
jwt: window.localStorage.getItem(LocalStorageKey) || (window.location.pathname === "/login" && window.location.hash.length > 1
|
||||||
|
? window.location.hash.substring(1) : null),
|
||||||
profile: null
|
profile: null
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import {useEffect} from "react";
|
|||||||
export function UserLogin() {
|
export function UserLogin() {
|
||||||
const auth = useSelector((state) => state.login.jwt);
|
const auth = useSelector((state) => state.login.jwt);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(auth){
|
if (auth) {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
}, [auth]);
|
}, [auth]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<Login/>
|
<Login/>
|
||||||
|
|||||||
Reference in New Issue
Block a user