void.cat/VoidCat/Services/Users/UserManager.cs

180 lines
5.6 KiB
C#
Raw Normal View History

using VoidCat.Database;
2022-02-21 22:35:06 +00:00
using VoidCat.Model;
using VoidCat.Services.Abstractions;
2022-09-08 09:41:31 +00:00
using VoidCat.Services.Users.Auth;
2022-02-21 22:35:06 +00:00
namespace VoidCat.Services.Users;
2022-09-08 09:41:31 +00:00
public class UserManager
2022-02-21 22:35:06 +00:00
{
private readonly IUserStore _store;
private readonly IEmailVerification _emailVerification;
2022-09-08 09:41:31 +00:00
private readonly OAuthFactory _oAuthFactory;
2023-10-13 19:07:35 +00:00
private readonly NostrProfileService _nostrProfile;
2022-02-24 12:00:28 +00:00
private static bool _checkFirstRegister;
2022-02-21 22:35:06 +00:00
2023-10-13 19:07:35 +00:00
public UserManager(IUserStore store, IEmailVerification emailVerification, OAuthFactory oAuthFactory, NostrProfileService nostrProfile)
2022-02-21 22:35:06 +00:00
{
_store = store;
_emailVerification = emailVerification;
2022-09-08 09:41:31 +00:00
_oAuthFactory = oAuthFactory;
2023-10-13 19:07:35 +00:00
_nostrProfile = nostrProfile;
2022-02-21 22:35:06 +00:00
}
2022-02-24 12:00:28 +00:00
2022-09-08 09:41:31 +00:00
/// <summary>
/// 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<User> Login(string email, string password)
2022-02-21 22:35:06 +00:00
{
var userId = await _store.LookupUser(email);
if (!userId.HasValue) throw new InvalidOperationException("User does not exist");
var user = await _store.Get(userId.Value);
2022-02-21 22:35:06 +00:00
if (!(user?.CheckPassword(password) ?? false)) throw new InvalidOperationException("User does not exist");
2022-02-24 12:00:28 +00:00
2022-09-08 09:41:31 +00:00
await HandleLogin(user);
2022-02-21 22:35:06 +00:00
return user;
}
2022-02-24 12:00:28 +00:00
2022-09-08 09:41:31 +00:00
/// <summary>
/// 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<User> Register(string email, string password)
2022-02-21 22:35:06 +00:00
{
var existingUser = await _store.LookupUser(email);
2022-06-08 16:17:53 +00:00
if (existingUser != Guid.Empty && existingUser != null)
throw new InvalidOperationException("User already exists");
2022-02-21 22:35:06 +00:00
var newUser = new User
2022-02-24 12:00:28 +00:00
{
2022-06-08 16:17:53 +00:00
Id = Guid.NewGuid(),
Email = email,
Password = password.HashPassword(),
Created = DateTime.UtcNow,
LastLogin = DateTime.UtcNow
2022-02-24 12:00:28 +00:00
};
2022-09-08 09:41:31 +00:00
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<User> LoginOrRegister(string code, string provider)
2022-09-08 09:41:31 +00:00
{
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);
2023-10-13 20:46:00 +00:00
if (uid.HasValue && uid.Value != Guid.Empty)
2022-09-08 09:41:31 +00:00
{
var existingUser = await _store.Get(uid.Value);
if (existingUser?.AuthType == UserAuthType.OAuth2)
2022-09-08 09:41:31 +00:00
{
return existingUser;
}
throw new InvalidOperationException("Auth failure, user type does not match!");
}
await SetupNewUser(user);
return user;
}
2023-10-13 19:07:35 +00:00
/// <summary>
/// Login or Register with nostr pubkey
/// </summary>
/// <param name="pubkey">Hex public key</param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async ValueTask<User> LoginOrRegister(string pubkey)
{
var uid = await _store.LookupUser(pubkey);
2023-10-13 20:46:00 +00:00
if (uid.HasValue && uid.Value != Guid.Empty)
2023-10-13 19:07:35 +00:00
{
var existingUser = await _store.Get(uid.Value);
if (existingUser?.AuthType == UserAuthType.Nostr)
{
return existingUser;
}
throw new InvalidOperationException("Auth failure, user type does not match!");
}
var profile = await _nostrProfile.FetchProfile(pubkey);
var newUser = new User
{
Id = Guid.NewGuid(),
AuthType = UserAuthType.Nostr,
Created = DateTime.UtcNow,
Avatar = profile?.Picture,
DisplayName = profile?.Name ?? "Nostrich",
Email = pubkey,
Flags = UserFlags.EmailVerified // always mark as email verififed
};
await SetupNewUser(newUser);
return newUser;
}
private async Task SetupNewUser(User newUser)
2022-09-08 09:41:31 +00:00
{
2022-02-24 12:00:28 +00:00
// automatically set first user to admin
if (!_checkFirstRegister)
{
_checkFirstRegister = true;
var users = await _store.ListUsers(new(0, 1));
if (users.TotalResults == 0)
{
2023-06-09 00:14:32 +00:00
newUser.Flags |= UserFlags.EmailVerified; // force email as verified for admin user
newUser.Roles.Add(new()
{
UserId = newUser.Id,
Role = Roles.Admin
});
2022-02-24 12:00:28 +00:00
}
}
2022-06-06 21:51:25 +00:00
await _store.Add(newUser);
2023-10-13 19:07:35 +00:00
if (!newUser.Flags.HasFlag(UserFlags.EmailVerified))
{
await _emailVerification.SendNewCode(newUser);
}
2022-09-08 09:41:31 +00:00
}
private async Task HandleLogin(User user)
2022-09-08 09:41:31 +00:00
{
user.LastLogin = DateTime.UtcNow;
2022-09-08 09:41:31 +00:00
await _store.UpdateLastLogin(user.Id, DateTime.UtcNow);
2022-02-21 22:35:06 +00:00
}
2023-10-13 19:07:35 +00:00
}