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]
|
[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))]
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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))]
|
||||||
|
|
|
@ -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,7 +58,9 @@ 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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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))
|
||||||
|
|
Loading…
Add table
Reference in a new issue