Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/apps/api/Accounts/AccountEndpoints.cs
diff options
context:
space:
mode:
authorLeitwolf112026-10-03 18:46:59 +0300
committerLeitwolf112026-10-03 18:46:59 +0300
commitb84a2216c26cc5392d0dc300b7a605ccb288d8c8 (patch)
tree048ae623f49cc5f2f94234d747351f9e659f5267 /apps/api/Accounts/AccountEndpoints.cs
parentec774db15976385a6d5ea6f48d3622fe507d3843 (diff)
parentcf444eb3bccf99c10f59ed5330b61c0397e0aeaa (diff)
downloadprogmaweb-fix/api-without-google.tar.gz
Merge remote-tracking branch 'origin/main' into fix/api-without-googlefix/api-without-google
Diffstat (limited to 'apps/api/Accounts/AccountEndpoints.cs')
-rw-r--r--apps/api/Accounts/AccountEndpoints.cs67
1 files changed, 67 insertions, 0 deletions
diff --git a/apps/api/Accounts/AccountEndpoints.cs b/apps/api/Accounts/AccountEndpoints.cs
index 06f4997..c5be51c 100644
--- a/apps/api/Accounts/AccountEndpoints.cs
+++ b/apps/api/Accounts/AccountEndpoints.cs
@@ -7,10 +7,29 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Google;
+/// <summary>HTTP endpoints of the account service under <c>/api/v1/accounts</c>.</summary>
+/// <remarks>
+/// <list type="table">
+/// <item><term><c>POST /register</c></term><description>Creates a password account and signs it in.</description></item>
+/// <item><term><c>POST /login</c></term><description>Signs in with email address and password.</description></item>
+/// <item><term><c>POST /logout</c></term><description>Ends the current session.</description></item>
+/// <item><term><c>GET /me</c></term><description>Returns the signed-in account.</description></item>
+/// <item><term><c>GET /oauth/google/start</c></term><description>Begins sign-in with Google.</description></item>
+/// <item><term><c>GET /oauth/google/complete</c></term><description>Finishes sign-in with Google.</description></item>
+/// </list>
+/// The endpoints that accept credentials share the <c>account-auth</c> rate limit.
+/// </remarks>
internal static class AccountEndpoints
{
+ /// <summary>
+ /// Authentication scheme of the short-lived cookie that carries the Google identity between the provider callback
+ /// and <c>/oauth/google/complete</c>.
+ /// </summary>
internal const string ExternalCookieScheme = "ProgmasoftExternal";
+ /// <summary>Registers the account endpoints.</summary>
+ /// <param name="endpoints">The route builder of the application.</param>
+ /// <returns>The same route builder, for chaining.</returns>
public static IEndpointRouteBuilder MapAccountEndpoints(this IEndpointRouteBuilder endpoints)
{
RouteGroupBuilder accounts = endpoints.MapGroup("/api/v1/accounts");
@@ -25,6 +44,15 @@ internal static class AccountEndpoints
return endpoints;
}
+ /// <summary>Begins sign-in with Google.</summary>
+ /// <param name="accountName">
+ /// Account name to create when the Google identity is new; omitted when signing in to an existing account.
+ /// </param>
+ /// <param name="configuration">Application configuration with the Google client credentials.</param>
+ /// <returns>
+ /// A challenge that sends the browser to Google, a redirect back to registration when the name is invalid, or
+ /// 503 when Google sign-in is not configured.
+ /// </returns>
private static IResult StartGoogleAsync(string? accountName, IConfiguration configuration)
{
if (string.IsNullOrWhiteSpace(configuration["Authentication:Google:ClientId"]) ||
@@ -54,6 +82,12 @@ internal static class AccountEndpoints
return Results.Challenge(properties, [GoogleDefaults.AuthenticationScheme]);
}
+ /// <summary>Finishes sign-in with Google after the provider callback.</summary>
+ /// <param name="accounts">Account service.</param>
+ /// <param name="sessions">Session service.</param>
+ /// <param name="context">The current request, holding the external cookie.</param>
+ /// <param name="cancellationToken">Cancels the operation.</param>
+ /// <returns>A redirect to the dashboard on success, or to the page that explains the failure.</returns>
private static async Task<IResult> CompleteGoogleAsync(
AccountService accounts,
SessionService sessions,
@@ -90,6 +124,13 @@ internal static class AccountEndpoints
return Results.Redirect($"/{Uri.EscapeDataString(authentication.Account.AccountName)}/dashboard");
}
+ /// <summary>Creates a password account and opens a session for it.</summary>
+ /// <param name="request">The registration body.</param>
+ /// <param name="accounts">Account service.</param>
+ /// <param name="sessions">Session service.</param>
+ /// <param name="context">The current request; receives the session cookie.</param>
+ /// <param name="cancellationToken">Cancels the operation.</param>
+ /// <returns>201 with the account, or a validation problem that names each refused field.</returns>
private static async Task<IResult> RegisterAsync(
RegisterAccountRequest request,
AccountService accounts,
@@ -110,6 +151,15 @@ internal static class AccountEndpoints
ToResponse(authentication.Account));
}
+ /// <summary>Signs in with email address and password.</summary>
+ /// <param name="request">The login body.</param>
+ /// <param name="accounts">Account service.</param>
+ /// <param name="sessions">Session service.</param>
+ /// <param name="context">The current request; receives the session cookie.</param>
+ /// <param name="cancellationToken">Cancels the operation.</param>
+ /// <returns>
+ /// 200 with the account, or 401 with one message that does not reveal whether the address or the password was wrong.
+ /// </returns>
private static async Task<IResult> LoginAsync(
LoginAccountRequest request,
AccountService accounts,
@@ -130,6 +180,11 @@ internal static class AccountEndpoints
return Results.Ok(ToResponse(authentication.Account));
}
+ /// <summary>Ends the current session and deletes its cookie.</summary>
+ /// <param name="sessions">Session service.</param>
+ /// <param name="context">The current request.</param>
+ /// <param name="cancellationToken">Cancels the operation.</param>
+ /// <returns>204, also when no session existed.</returns>
private static async Task<IResult> LogoutAsync(
SessionService sessions,
HttpContext context,
@@ -147,6 +202,11 @@ internal static class AccountEndpoints
return Results.NoContent();
}
+ /// <summary>Returns the account of the current session.</summary>
+ /// <param name="sessions">Session service.</param>
+ /// <param name="context">The current request.</param>
+ /// <param name="cancellationToken">Cancels the operation.</param>
+ /// <returns>200 with the account, or 401 when the request has no valid session.</returns>
private static async Task<IResult> MeAsync(
SessionService sessions,
HttpContext context,
@@ -157,6 +217,10 @@ internal static class AccountEndpoints
return account is null ? Results.Unauthorized() : Results.Ok(ToResponse(account));
}
+ /// <summary>Writes the session cookie of a new session to the response.</summary>
+ /// <param name="context">The current request.</param>
+ /// <param name="sessions">Session service, which names the cookie.</param>
+ /// <param name="authentication">The session that was opened.</param>
private static void WriteSessionCookie(
HttpContext context,
SessionService sessions,
@@ -166,6 +230,9 @@ internal static class AccountEndpoints
authentication.SessionToken,
SessionService.CreateCookieOptions(authentication.ExpiresAt));
+ /// <summary>Projects a stored account to its public view.</summary>
+ /// <param name="account">The stored account.</param>
+ /// <returns>The fields a client may see.</returns>
private static AccountResponse ToResponse(AccountRecord account) =>
new(account.AccountName, account.Email, account.CreatedAt);
}