Add support for registration invites
This commit is contained in:
parent
3c72d50459
commit
3a466b2e0c
12 changed files with 70 additions and 44 deletions
|
@ -28,14 +28,17 @@ public class AuthController(DatabaseContext db, UserService userSvc) : Controlle
|
|||
[Consumes(MediaTypeNames.Application.Json)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TimelineResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> Login([FromBody] AuthRequest request) {
|
||||
var user = await db.Users.FirstOrDefaultAsync(p => p.Host == null &&
|
||||
p.UsernameLower == request.Username.ToLowerInvariant());
|
||||
if (user == null) return Unauthorized();
|
||||
if (user == null)
|
||||
return StatusCode(StatusCodes.Status403Forbidden);
|
||||
var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user);
|
||||
if (profile?.Password == null) return Unauthorized();
|
||||
if (!AuthHelpers.ComparePassword(request.Password, profile.Password)) return Unauthorized();
|
||||
if (profile?.Password == null)
|
||||
return StatusCode(StatusCodes.Status403Forbidden);
|
||||
if (!AuthHelpers.ComparePassword(request.Password, profile.Password))
|
||||
return StatusCode(StatusCodes.Status403Forbidden);
|
||||
|
||||
var res = await db.AddAsync(new Session {
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
|
@ -65,11 +68,12 @@ public class AuthController(DatabaseContext db, UserService userSvc) : Controlle
|
|||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TimelineResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> Register([FromBody] AuthRequest request) {
|
||||
//TODO: captcha support
|
||||
//TODO: invite support
|
||||
|
||||
await userSvc.CreateLocalUser(request.Username, request.Password);
|
||||
await userSvc.CreateLocalUser(request.Username, request.Password, request.Invite);
|
||||
return await Login(request);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
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.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||
|
||||
[ApiController]
|
||||
[Tags("Mastodon")]
|
||||
[EnableRateLimiting("sliding")]
|
||||
[Produces("application/json")]
|
||||
[Route("/api/v1")]
|
||||
public class MastodonAuthController() : Controller {
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Mastodon;
|
||||
|
||||
[ApiController]
|
||||
[Tags("Mastodon")]
|
||||
[EnableRateLimiting("sliding")]
|
||||
[Produces("application/json")]
|
||||
[Route("/api/v1")]
|
||||
public class MastodonAuthController : Controller { }
|
|
@ -3,6 +3,7 @@ using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
|||
namespace Iceshrimp.Backend.Controllers.Schemas;
|
||||
|
||||
public class AuthRequest {
|
||||
[J("username")] public required string Username { get; set; }
|
||||
[J("password")] public required string Password { get; set; }
|
||||
[J("username")] public required string Username { get; set; }
|
||||
[J("password")] public required string Password { get; set; }
|
||||
[J("invite")] public string? Invite { get; set; }
|
||||
}
|
|
@ -36,8 +36,9 @@ public sealed class Config {
|
|||
}
|
||||
|
||||
public sealed class SecuritySection {
|
||||
public required bool AuthorizedFetch { get; init; } = true;
|
||||
public required ExceptionVerbosity ExceptionVerbosity { get; init; } = ExceptionVerbosity.Basic;
|
||||
public required bool AuthorizedFetch { get; init; } = true;
|
||||
public required ExceptionVerbosity ExceptionVerbosity { get; init; } = ExceptionVerbosity.Basic;
|
||||
public required Enums.Registrations Registrations { get; init; } = Enums.Registrations.Closed;
|
||||
}
|
||||
|
||||
public sealed class DatabaseSection {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
namespace Iceshrimp.Backend.Core.Configuration;
|
||||
|
||||
public class Constants {
|
||||
public const string UserAgent = "Iceshrimp/2024.1-experimental (https://shrimp-next.fedi.solutions)";
|
||||
public static readonly string[] SystemUsers = { "instance.actor", "relay.actor" };
|
||||
public static class Constants {
|
||||
public const string ActivityStreamsNs = "https://www.w3.org/ns/activitystreams";
|
||||
public static readonly string[] SystemUsers = { "instance.actor", "relay.actor" };
|
||||
}
|
9
Iceshrimp.Backend/Core/Configuration/Enums.cs
Normal file
9
Iceshrimp.Backend/Core/Configuration/Enums.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Iceshrimp.Backend.Core.Configuration;
|
||||
|
||||
public static class Enums {
|
||||
public enum Registrations {
|
||||
Closed = 0,
|
||||
Invite = 1,
|
||||
Open = 2
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using JC = Newtonsoft.Json.JsonConverterAttribute;
|
||||
|
||||
|
@ -13,7 +14,7 @@ public class ASActivity : ASObject {
|
|||
public ASObject? Object { get; set; }
|
||||
|
||||
public static class Types {
|
||||
private const string Ns = "https://www.w3.org/ns/activitystreams";
|
||||
private const string Ns = Constants.ActivityStreamsNs;
|
||||
|
||||
public const string Create = $"{Ns}#Create";
|
||||
public const string Delete = $"{Ns}#Delete";
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using JC = Newtonsoft.Json.JsonConverterAttribute;
|
||||
|
@ -134,7 +135,7 @@ public class ASActor : ASObject {
|
|||
}
|
||||
|
||||
public static class Types {
|
||||
private const string Ns = "https://www.w3.org/ns/activitystreams";
|
||||
private const string Ns = Constants.ActivityStreamsNs;
|
||||
|
||||
public const string Application = $"{Ns}#Application";
|
||||
public const string Group = $"{Ns}#Group";
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using JC = Newtonsoft.Json.JsonConverterAttribute;
|
||||
|
@ -55,7 +56,7 @@ public class ASNote : ASObject {
|
|||
}
|
||||
|
||||
public static class Types {
|
||||
private const string Ns = "https://www.w3.org/ns/activitystreams";
|
||||
private const string Ns = Constants.ActivityStreamsNs;
|
||||
|
||||
public const string Note = $"{Ns}#Note";
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
|
@ -13,7 +14,9 @@ using Microsoft.Extensions.Options;
|
|||
namespace Iceshrimp.Backend.Core.Services;
|
||||
|
||||
public class UserService(
|
||||
IOptions<Config.InstanceSection> config,
|
||||
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")]
|
||||
IOptionsSnapshot<Config.SecuritySection> security,
|
||||
IOptions<Config.InstanceSection> instance,
|
||||
ILogger<UserService> logger,
|
||||
DatabaseContext db,
|
||||
ActivityFetcherService fetchSvc) {
|
||||
|
@ -28,8 +31,8 @@ public class UserService(
|
|||
|
||||
public async Task<User?> GetUserFromQuery(string query) {
|
||||
if (query.StartsWith("http://") || query.StartsWith("https://"))
|
||||
if (query.StartsWith($"https://{config.Value.WebDomain}/users/")) {
|
||||
query = query[$"https://{config.Value.WebDomain}/users/".Length..];
|
||||
if (query.StartsWith($"https://{instance.Value.WebDomain}/users/")) {
|
||||
query = query[$"https://{instance.Value.WebDomain}/users/".Length..];
|
||||
return await db.Users.FirstOrDefaultAsync(p => p.Id == query) ??
|
||||
throw GracefulException.NotFound("User not found");
|
||||
}
|
||||
|
@ -38,7 +41,7 @@ public class UserService(
|
|||
}
|
||||
|
||||
var tuple = AcctToTuple(query);
|
||||
if (tuple.Host == config.Value.WebDomain || tuple.Host == config.Value.AccountDomain)
|
||||
if (tuple.Host == instance.Value.WebDomain || tuple.Host == instance.Value.AccountDomain)
|
||||
tuple.Host = null;
|
||||
return await db.Users.FirstOrDefaultAsync(p => p.Username == tuple.Username && p.Host == tuple.Host);
|
||||
}
|
||||
|
@ -97,13 +100,21 @@ public class UserService(
|
|||
return user;
|
||||
}
|
||||
|
||||
public async Task<User> CreateLocalUser(string username, string password) {
|
||||
public async Task<User> CreateLocalUser(string username, string password, string? invite) {
|
||||
//TODO: invite system should allow multi-use invites & time limited invites
|
||||
if (security.Value.Registrations == Enums.Registrations.Closed)
|
||||
throw new GracefulException(HttpStatusCode.Forbidden, "Registrations are disabled on this server");
|
||||
if (security.Value.Registrations == Enums.Registrations.Invite && invite == null)
|
||||
throw new GracefulException(HttpStatusCode.Forbidden, "Request is missing the invite code");
|
||||
if (security.Value.Registrations == Enums.Registrations.Invite &&
|
||||
!await db.RegistrationTickets.AnyAsync(p => p.Code == invite))
|
||||
throw new GracefulException(HttpStatusCode.Forbidden, "The specified invite code is invalid");
|
||||
if (username.Contains('.'))
|
||||
throw new GracefulException(HttpStatusCode.BadRequest, "Username must not contain the dot character");
|
||||
|
||||
if (Constants.SystemUsers.Contains(username.ToLowerInvariant()))
|
||||
throw new GracefulException(HttpStatusCode.BadRequest, "Username must not be a system user");
|
||||
if (await db.Users.AnyAsync(p => p.Host == null && p.UsernameLower == username.ToLowerInvariant()))
|
||||
throw new GracefulException(HttpStatusCode.BadRequest, "User already exists");
|
||||
|
||||
if (await db.UsedUsernames.AnyAsync(p => p.Username.ToLower() == username.ToLowerInvariant()))
|
||||
throw new GracefulException(HttpStatusCode.BadRequest, "Username was already used");
|
||||
|
||||
|
@ -132,6 +143,9 @@ public class UserService(
|
|||
Username = username.ToLowerInvariant()
|
||||
};
|
||||
|
||||
var ticket = await db.RegistrationTickets.FirstAsync(p => p.Code == invite);
|
||||
|
||||
db.Remove(ticket);
|
||||
await db.AddRangeAsync(user, userKeypair, userProfile, usedUsername);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
|
|
|
@ -7,10 +7,14 @@ AccountDomain = example.org
|
|||
;; Whether to require incoming ActivityPub requests carry a valid HTTP or LD signature
|
||||
AuthorizedFetch = true
|
||||
|
||||
;; The level of detail in API error responses.
|
||||
;; The level of detail in API error responses
|
||||
;; Options: [None, Basic, Full]
|
||||
ExceptionVerbosity = Basic
|
||||
|
||||
;; Whether to allow instance registrations
|
||||
;; Options: [Closed, Invite, Open]
|
||||
Registrations = Closed
|
||||
|
||||
[Database]
|
||||
Host = localhost
|
||||
Database = iceshrimp
|
||||
|
|
Loading…
Add table
Reference in a new issue