Add create local user function

This commit is contained in:
Laura Hausmann 2024-01-23 21:24:45 +01:00
parent e77da0f91d
commit c35b62e2f2
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
7 changed files with 89 additions and 32 deletions

View file

@ -12,10 +12,7 @@ namespace Iceshrimp.Backend.Controllers;
[ApiController] [ApiController]
[MediaTypeRouteFilter("application/activity+json", "application/ld+json")] [MediaTypeRouteFilter("application/activity+json", "application/ld+json")]
[Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")] [Produces("application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")]
public class ActivityPubController( public class ActivityPubController(DatabaseContext db, APUserRenderer userRenderer) : Controller {
ILogger<ActivityPubController> logger,
DatabaseContext db,
APUserRenderer userRenderer) : Controller {
/* /*
[HttpGet("/notes/{id}")] [HttpGet("/notes/{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Note))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Note))]

View file

@ -1,10 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Net; using System.Net;
using System.Net.Mime; using System.Net.Mime;
using Iceshrimp.Backend.Controllers.Schemas; using Iceshrimp.Backend.Controllers.Schemas;
using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -13,10 +13,10 @@ namespace Iceshrimp.Backend.Controllers;
[ApiController] [ApiController]
[Produces("application/json")] [Produces("application/json")]
[Route("/api/iceshrimp/v1/auth")] [Route("/api/iceshrimp/v1/auth")]
public class AuthController(DatabaseContext db) : Controller { public class AuthController(DatabaseContext db, UserService userSvc) : Controller {
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
public async Task<IActionResult> GetAuthStatus() { public IActionResult GetAuthStatus() {
return new StatusCodeResult((int)HttpStatusCode.NotImplemented); return new StatusCodeResult((int)HttpStatusCode.NotImplemented);
} }
@ -25,11 +25,9 @@ public class AuthController(DatabaseContext db) : Controller {
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TimelineResponse))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TimelineResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status401Unauthorized, 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) { public async Task<IActionResult> Login([FromBody] AuthRequest request) {
var user = await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == request.Username.ToLowerInvariant() && var user = await db.Users.FirstOrDefaultAsync(p => p.Host == null &&
p.Host == null); p.UsernameLower == request.Username.ToLowerInvariant());
if (user == null) return Unauthorized(); if (user == null) return Unauthorized();
var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.UserId == user.Id); var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.UserId == user.Id);
if (profile?.Password == null) return Unauthorized(); 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
} }

View file

@ -8,10 +8,7 @@ using Microsoft.Extensions.Options;
namespace Iceshrimp.Backend.Controllers.Renderers.ActivityPub; namespace Iceshrimp.Backend.Controllers.Renderers.ActivityPub;
public class APUserRenderer( public class APUserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db) {
IOptions<Config.InstanceSection> config,
ILogger<APUserRenderer> logger,
DatabaseContext db) {
public async Task<ASActor> Render(User user) { public async Task<ASActor> Render(User user) {
if (user.Host != null) throw new Exception("Refusing to render remote user"); if (user.Host != null) throw new Exception("Refusing to render remote user");

View file

@ -9,7 +9,7 @@ namespace Iceshrimp.Backend.Controllers;
[ApiController] [ApiController]
[Produces("application/json")] [Produces("application/json")]
[Route("/api/iceshrimp/v1/user/{id}")] [Route("/api/iceshrimp/v1/user/{id}")]
public class UserController(ILogger<UserController> logger, DatabaseContext db) : Controller { public class UserController(DatabaseContext db) : Controller {
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserResponse))]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))] [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]

View file

@ -1,11 +1,10 @@
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Federation.WebFinger; using Iceshrimp.Backend.Core.Federation.WebFinger;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
namespace Iceshrimp.Backend.Core.Federation.ActivityPub; 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: * The full web finger algorithm:
* *
@ -59,23 +58,25 @@ public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, Web
return (finalAcct, finalUri); 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) { public async Task<User> Resolve(string query) {
query = NormalizeQuery(query); query = NormalizeQuery(query);
// First, let's see if we already know the user // First, let's see if we already know the user
var user = await userSvc.GetUserFromQuery(query); var user = await userSvc.GetUserFromQuery(query);
if (user != null) return user; if (user != null) return user;
// We don't, so we need to run WebFinger // We don't, so we need to run WebFinger
var (acct, uri) = await WebFinger(query); var (acct, uri) = await WebFinger(query);
// Check the database again with the new data // Check the database again with the new data
if (uri != query) user = await userSvc.GetUserFromQuery(uri); if (uri != query) user = await userSvc.GetUserFromQuery(uri);
if (user == null && acct != query) await userSvc.GetUserFromQuery(acct); if (user == null && acct != query) await userSvc.GetUserFromQuery(acct);
if (user != null) return user; if (user != null) return user;
// Pass the job on to userSvc, which will create the user // Pass the job on to userSvc, which will create the user
return await userSvc.CreateUser(uri, acct); return await userSvc.CreateUser(uri, acct);
} }

View file

@ -4,9 +4,14 @@ using Isopoh.Cryptography.Argon2;
namespace Iceshrimp.Backend.Core.Helpers; namespace Iceshrimp.Backend.Core.Helpers;
public static class AuthHelpers { public static class AuthHelpers {
// TODO: Implement legacy hash detection // TODO: Implement legacy (bcrypt) hash detection
[SuppressMessage("ReSharper.DPA", "DPA0003: Excessive memory allocations in LOH")] [SuppressMessage("ReSharper.DPA", "DPA0003: Excessive memory allocations in LOH")]
public static bool ComparePassword(string password, string hash) { public static bool ComparePassword(string password, string hash) {
return Argon2.Verify(hash, password); 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);
}
} }

View file

@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services; 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) { private static (string Username, string Host) AcctToTuple(string acct) {
if (!acct.StartsWith("acct:")) throw new Exception("Invalid query"); 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) { 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); return db.Users.FirstOrDefaultAsync(p => p.Uri == query);
}
var tuple = AcctToTuple(query); var tuple = AcctToTuple(query);
return db.Users.FirstOrDefaultAsync(p => p.Username == tuple.Username && p.Host == tuple.Host); 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 //FollowersCount
//FollowingCount //FollowingCount
Emojis = [], //FIXME Emojis = [], //FIXME
Tags = [], //FIXME Tags = [] //FIXME
}; };
//TODO: add UserProfile as well //TODO: add UserProfile as well
@ -68,13 +67,58 @@ public class UserService(ILogger<UserService> logger, DatabaseContext db, HttpCl
return user; return user;
} }
public async Task<User> GetInstanceActor() => await GetOrCreateSystemUser("instance.actor"); public async Task<User> CreateLocalUser(string username, string password) {
public async Task<User> GetRelayActor() => await GetOrCreateSystemUser("relay.actor"); 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 //TODO: cache in redis
private async Task<User> GetOrCreateSystemUser(string username) => private async Task<User> GetOrCreateSystemUser(string username) {
await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username && p.Host == null) ?? return await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username && p.Host == null) ??
await CreateSystemUser(username); await CreateSystemUser(username);
}
private async Task<User> CreateSystemUser(string username) { private async Task<User> CreateSystemUser(string username) {
if (await db.Users.AnyAsync(p => p.UsernameLower == username.ToLowerInvariant() && p.Host == null)) if (await db.Users.AnyAsync(p => p.UsernameLower == username.ToLowerInvariant() && p.Host == null))