feat: first-run claim wizard (no default admin, claim code from the backend log); owner list shows account and passkey; container install first [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
This commit is contained in:
1 parent
2ae63653c4
commit
f22fdf8b68
31 files changed
+799
-49
No files matched your search
@@ -19,6 +19,14 @@ public static class AuthEndpoints
|
||||
.AllowsAnonymous()
|
||||
.Api("auth_login", "Sign in with a username and password", Tag)
|
||||
.Returns<LoginResponse>().ProducesProblems(400, 401, 429);
|
||||
open.MapGet("/auth/setup", SetupStatusAsync)
|
||||
.AllowsAnonymous()
|
||||
.Api("setup_status", "Whether the panel has no users yet and still has to be claimed", Tag)
|
||||
.Returns<SetupStatusDto>();
|
||||
open.MapPost("/auth/setup", SetupAsync)
|
||||
.AllowsAnonymous()
|
||||
.Api("setup_complete", "Create the first administrator (needs the claim code from the backend's log); the same response as the password sign-in. Closed once the panel is claimed (a user exists)", Tag)
|
||||
.Returns<LoginResponse>(StatusCodes.Status201Created).ProducesProblems(400, 401, 409, 429);
|
||||
open.MapPost("/auth/passkey/options", PasskeyLoginOptions)
|
||||
.AllowsAnonymous()
|
||||
.Api("auth_passkey_options", "A challenge for signing in with a passkey (discoverable credential, no username needed)", Tag)
|
||||
@@ -77,6 +85,32 @@ public static class AuthEndpoints
|
||||
return IssueSession(http, login, request.ReturnSession == true, options);
|
||||
}
|
||||
|
||||
private static async Task<IResult> SetupStatusAsync(SetupService setup, CancellationToken ct)
|
||||
{
|
||||
return TypedResults.Json(new SetupStatusDto(await setup.NeedsSetupAsync(ct)), ApiJsonContext.Default.SetupStatusDto);
|
||||
}
|
||||
|
||||
private static async Task<IResult> SetupAsync(HttpContext http, SetupRequest request, SetupService setup, AuditService audit, LoginRateLimiter limiter, BackendOptions options)
|
||||
{
|
||||
IResult limit = Limited(http, limiter, out bool ok, "setup:");
|
||||
if (!ok)
|
||||
{
|
||||
return limit;
|
||||
}
|
||||
|
||||
string remote = http.ClientAddress();
|
||||
Result<LoginResult> result = await setup.CompleteAsync(request.ClaimCode, request.Username ?? "", request.Password ?? "", http.RequestAborted);
|
||||
if (!result.TryGet(out LoginResult? login, out FleetError? error))
|
||||
{
|
||||
await audit.RecordAsync(null, request.Username ?? "", AuthMethods.Password, "setup.complete", null, null, remote, false, http.RequestAborted);
|
||||
return Problems.From(http, error);
|
||||
}
|
||||
|
||||
await audit.RecordAsync(login.UserId, login.Username, AuthMethods.Password, "setup.complete", null, null, remote, true, http.RequestAborted);
|
||||
Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<LoginResponse> session = IssueSession(http, login, request.ReturnSession == true, options);
|
||||
return TypedResults.Json(session.Value!, ApiJsonContext.Default.LoginResponse, statusCode: StatusCodes.Status201Created);
|
||||
}
|
||||
|
||||
/// <summary>发放会话:Cookie(同源前端)或在响应里返回会话令牌(跨域前端的 return_session)。密码登录和 passkey 登录共用。</summary>
|
||||
private static Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<LoginResponse> IssueSession(HttpContext http, LoginResult login, bool returnSession, BackendOptions options)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user