Add create local user function
This commit is contained in:
parent
e77da0f91d
commit
c35b62e2f2
7 changed files with 89 additions and 32 deletions
|
@ -12,10 +12,7 @@ namespace Iceshrimp.Backend.Controllers;
|
|||
[ApiController]
|
||||
[MediaTypeRouteFilter("application/activity+json", "application/ld+json")]
|
||||
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
|
||||
public class ActivityPubController(
|
||||
ILogger<ActivityPubController> logger,
|
||||
DatabaseContext db,
|
||||
APUserRenderer userRenderer) : Controller {
|
||||
public class ActivityPubController(DatabaseContext db, APUserRenderer userRenderer) : Controller {
|
||||
/*
|
||||
[HttpGet("/notes/{id}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Note))]
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
using Iceshrimp.Backend.Controllers.Schemas;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Helpers;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
@ -13,10 +13,10 @@ namespace Iceshrimp.Backend.Controllers;
|
|||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("/api/iceshrimp/v1/auth")]
|
||||
public class AuthController(DatabaseContext db) : Controller {
|
||||
public class AuthController(DatabaseContext db, UserService userSvc) : Controller {
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
|
||||
public async Task<IActionResult> GetAuthStatus() {
|
||||
public IActionResult GetAuthStatus() {
|
||||
return new StatusCodeResult((int)HttpStatusCode.NotImplemented);
|
||||
}
|
||||
|
||||
|
@ -25,11 +25,9 @@ public class AuthController(DatabaseContext db) : Controller {
|
|||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TimelineResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
[SuppressMessage("Performance",
|
||||
"CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons")]
|
||||
public async Task<IActionResult> Login([FromBody] AuthRequest request) {
|
||||
var user = await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == request.Username.ToLowerInvariant() &&
|
||||
p.Host == null);
|
||||
var user = await db.Users.FirstOrDefaultAsync(p => p.Host == null &&
|
||||
p.UsernameLower == request.Username.ToLowerInvariant());
|
||||
if (user == null) return Unauthorized();
|
||||
var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.UserId == user.Id);
|
||||
if (profile?.Password == null) return Unauthorized();
|
||||
|
@ -56,4 +54,19 @@ public class AuthController(DatabaseContext db) : Controller {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TimelineResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> Register([FromBody] AuthRequest request) {
|
||||
//TODO: captcha support
|
||||
//TODO: invite support
|
||||
|
||||
await userSvc.CreateLocalUser(request.Username, request.Password);
|
||||
return await Login(request);
|
||||
}
|
||||
|
||||
//TODO: PATCH = update password
|
||||
}
|
|
@ -8,10 +8,7 @@ using Microsoft.Extensions.Options;
|
|||
|
||||
namespace Iceshrimp.Backend.Controllers.Renderers.ActivityPub;
|
||||
|
||||
public class APUserRenderer(
|
||||
IOptions<Config.InstanceSection> config,
|
||||
ILogger<APUserRenderer> logger,
|
||||
DatabaseContext db) {
|
||||
public class APUserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db) {
|
||||
public async Task<ASActor> Render(User user) {
|
||||
if (user.Host != null) throw new Exception("Refusing to render remote user");
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace Iceshrimp.Backend.Controllers;
|
|||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("/api/iceshrimp/v1/user/{id}")]
|
||||
public class UserController(ILogger<UserController> logger, DatabaseContext db) : Controller {
|
||||
public class UserController(DatabaseContext db) : Controller {
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Federation.WebFinger;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||
|
||||
public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, WebFingerService webFingerSvc, DatabaseContext db) {
|
||||
public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, WebFingerService webFingerSvc) {
|
||||
/*
|
||||
* The full web finger algorithm:
|
||||
*
|
||||
|
@ -59,7 +58,9 @@ public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, Web
|
|||
return (finalAcct, finalUri);
|
||||
}
|
||||
|
||||
private static string NormalizeQuery(string query) => query.StartsWith('@') ? $"acct:{query[1..]}" : query;
|
||||
private static string NormalizeQuery(string query) {
|
||||
return query.StartsWith('@') ? $"acct:{query[1..]}" : query;
|
||||
}
|
||||
|
||||
public async Task<User> Resolve(string query) {
|
||||
query = NormalizeQuery(query);
|
||||
|
|
|
@ -4,9 +4,14 @@ using Isopoh.Cryptography.Argon2;
|
|||
namespace Iceshrimp.Backend.Core.Helpers;
|
||||
|
||||
public static class AuthHelpers {
|
||||
// TODO: Implement legacy hash detection
|
||||
// TODO: Implement legacy (bcrypt) hash detection
|
||||
[SuppressMessage("ReSharper.DPA", "DPA0003: Excessive memory allocations in LOH")]
|
||||
public static bool ComparePassword(string password, string hash) {
|
||||
return Argon2.Verify(hash, password);
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper.DPA", "DPA0003: Excessive memory allocations in LOH")]
|
||||
public static string HashPassword(string password) {
|
||||
return Argon2.Hash(password, parallelism: 4);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
|
||||
namespace Iceshrimp.Backend.Core.Services;
|
||||
|
||||
public class UserService(ILogger<UserService> logger, DatabaseContext db, HttpClient client, ActivityPubService apSvc) {
|
||||
public class UserService(ILogger<UserService> logger, DatabaseContext db, ActivityPubService apSvc) {
|
||||
private static (string Username, string Host) AcctToTuple(string acct) {
|
||||
if (!acct.StartsWith("acct:")) throw new Exception("Invalid query");
|
||||
|
||||
|
@ -18,9 +18,8 @@ public class UserService(ILogger<UserService> logger, DatabaseContext db, HttpCl
|
|||
}
|
||||
|
||||
public Task<User?> GetUserFromQuery(string query) {
|
||||
if (query.StartsWith("http://") || query.StartsWith("https://")) {
|
||||
if (query.StartsWith("http://") || query.StartsWith("https://"))
|
||||
return db.Users.FirstOrDefaultAsync(p => p.Uri == query);
|
||||
}
|
||||
|
||||
var tuple = AcctToTuple(query);
|
||||
return db.Users.FirstOrDefaultAsync(p => p.Username == tuple.Username && p.Host == tuple.Host);
|
||||
|
@ -57,7 +56,7 @@ public class UserService(ILogger<UserService> logger, DatabaseContext db, HttpCl
|
|||
//FollowersCount
|
||||
//FollowingCount
|
||||
Emojis = [], //FIXME
|
||||
Tags = [], //FIXME
|
||||
Tags = [] //FIXME
|
||||
};
|
||||
|
||||
//TODO: add UserProfile as well
|
||||
|
@ -68,13 +67,58 @@ public class UserService(ILogger<UserService> logger, DatabaseContext db, HttpCl
|
|||
return user;
|
||||
}
|
||||
|
||||
public async Task<User> GetInstanceActor() => await GetOrCreateSystemUser("instance.actor");
|
||||
public async Task<User> GetRelayActor() => await GetOrCreateSystemUser("relay.actor");
|
||||
public async Task<User> CreateLocalUser(string username, string password) {
|
||||
if (await db.Users.AnyAsync(p => p.Host == null && p.UsernameLower == username.ToLowerInvariant()))
|
||||
throw new Exception("User already exists");
|
||||
|
||||
if (await db.UsedUsernames.AnyAsync(p => p.Username.ToLower() == username.ToLowerInvariant()))
|
||||
throw new Exception("Username was already used");
|
||||
|
||||
var keypair = RSA.Create(4096);
|
||||
var user = new User {
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Username = username,
|
||||
UsernameLower = username.ToLowerInvariant(),
|
||||
Host = null
|
||||
};
|
||||
|
||||
var userKeypair = new UserKeypair {
|
||||
UserId = user.Id,
|
||||
PrivateKey = keypair.ExportPkcs8PrivateKeyPem(),
|
||||
PublicKey = keypair.ExportSubjectPublicKeyInfoPem()
|
||||
};
|
||||
|
||||
var userProfile = new UserProfile {
|
||||
UserId = user.Id,
|
||||
Password = AuthHelpers.HashPassword(password)
|
||||
};
|
||||
|
||||
var usedUsername = new UsedUsername {
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Username = username.ToLowerInvariant()
|
||||
};
|
||||
|
||||
await db.AddRangeAsync(user, userKeypair, userProfile, usedUsername);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
public async Task<User> GetInstanceActor() {
|
||||
return await GetOrCreateSystemUser("instance.actor");
|
||||
}
|
||||
|
||||
public async Task<User> GetRelayActor() {
|
||||
return await GetOrCreateSystemUser("relay.actor");
|
||||
}
|
||||
|
||||
//TODO: cache in redis
|
||||
private async Task<User> GetOrCreateSystemUser(string username) =>
|
||||
await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username && p.Host == null) ??
|
||||
private async Task<User> GetOrCreateSystemUser(string username) {
|
||||
return await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username && p.Host == null) ??
|
||||
await CreateSystemUser(username);
|
||||
}
|
||||
|
||||
private async Task<User> CreateSystemUser(string username) {
|
||||
if (await db.Users.AnyAsync(p => p.UsernameLower == username.ToLowerInvariant() && p.Host == null))
|
||||
|
|
Loading…
Add table
Reference in a new issue