1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
using System.Diagnostics;
using System.Text.Json.Nodes;
using ArcaneLibs.Collections;
using ArcaneLibs.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Spacebar.Interop.Authentication.AspNetCore;
using Spacebar.Models.Db.Contexts;
using Spacebar.DataMappings.Generic;
using Spacebar.Interop.Authentication;
using Spacebar.Models.Api;
using Spacebar.Models.Generic;
namespace Spacebar.UApi.Controllers;
[Route("/api/v{_}/auth/register")]
[Route("/api/auth/register")]
[ApiController]
public class RegisterController(ILogger<RegisterController> logger, SpacebarDbContext db, SpacebarAuthenticationService authService) : ControllerBase {
private static ExpiringSemaphoreCache<RegisterResponse> _sem = new();
[HttpPost]
public async Task<RegisterResponse> GetMemberAsync(RegisterRequest req) {
Task<RegisterResponse> task;
lock (_sem)
task = _sem.GetOrAdd($"{req.Email}{req.DateOfBirth}{req.Username}", async () => {
var sw = Stopwatch.StartNew();
// TODO: reg tokens - do we even want to continue supporting referrer-based tokens?
// TODO: config: register.allowNewRegistration
if (!req.Consent)
throw new SpacebarApiException("Consent is required to continue") {
Code = 0, Request = Request.Path, Errors = new() {
{
"consent",
new JsonObject() {
{ "code", "CONSENT_REQUIRED" },
{ "message", "You must consent to register on this instance." }
}
}
}
};
// TODO: config reg.disabled
// TODO: captchas
// TODO: do we even want to support multiaccounts on a single email?
// TODO: ip checks
// TODO: gift_code_sku_id?
// TODO: check password strength
// TODO: ability to config DoB checks
if (!req.DateOfBirth.HasValue || (DateTime.UtcNow - req.DateOfBirth.Value.ToDateTime(new TimeOnly(0, 0, 0))).TotalDays < (13 * 365))
throw new SpacebarApiException("Invalid date of birth") {
Code = 0, Request = Request.Path, Errors = new() {
{
"date_of_birth",
new JsonObject() {
{ "code", "DATE_OF_BIRTH_INVALID" },
{ "message", "You must enter a valid date of birth to continue" }
}
}
}
};
// TODO: password min length config, optional password
if (string.IsNullOrWhiteSpace(req.Password) || req.Password!.Length < 4)
throw new SpacebarApiException("Invalid password") {
Code = 0, Request = Request.Path, Errors = new() {
{
"password",
new JsonObject() {
{ "code", "PASSWORD_REQUIREMENTS_MIN_LENGTH" },
{ "message", "Your password must be at least 4 characters." }
}
}
}
};
// TODO: require invite
// TODO: global register ratelimit
// TODO: configurable username length limits
if (string.IsNullOrWhiteSpace(req.Username) || req.Username.Length < 4 || req.Username.Length > 255)
throw new SpacebarApiException("Invalid username length") {
Code = 0, Request = Request.Path, Errors = new() {
{
"username",
new JsonObject() {
{ "code", "BASE_TYPE_BAD_LENGTH" },
{ "message", "Username must be between 4 and 255 characters." }
}
}
}
};
var existingDiscrims = await db.Users.Where(x => x.Username == req.Username).ToListAsync();
// TODO: throw error if out of slots
string newDiscrim;
do {
newDiscrim = Random.Shared.Next(0, 1000).ToString("0000");
} while (existingDiscrims.Any(x => x.Discriminator == newDiscrim));
var user = db.Users.Add(new() {
Username = req.Username,
Discriminator = newDiscrim,
Email = req.Email,
Id = GenerateSnowflake(),
Data = new JsonObject() {
{ "hash", BCrypt.Net.BCrypt.HashPassword(req.Password, 12) },
{ "valid_tokens_since", DateTime.Now }
}.ToJson(),
// settings?
Premium = true,
PremiumSince = DateTime.Now,
PremiumType = 2,
Verified = true,
CreatedAt = DateTime.Now,
Bot = false,
Fingerprints = new(),
Bio = ""
});
await db.SaveChangesAsync();
// TODO: verification email
// TODO: autojoin
// TODO: handle invite key
return new RegisterResponse() {
Token = await authService.GenerateAccessTokenAsync(user.Entity.Id)
};
},
TimeSpan.FromMinutes(1));
return await task;
}
// TODO: move
private static long _snowflakeIdx = 0;
public static long GenerateSnowflake() {
_snowflakeIdx %= 4095;
return
((DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeMilliseconds()) << 22)
| ((long)(Environment.CurrentManagedThreadId % 31) << 17) //worker ID
| ((long)(Environment.ProcessId % 31) << 12) // process ID
| ((long)_snowflakeIdx++)
;
}
}
|