From 55f921b0477d4d8062c5db5332383b6986a6f66f Mon Sep 17 00:00:00 2001 From: Thomas Jespersen Date: Mon, 22 Jul 2024 11:51:31 +0200 Subject: [PATCH] Make account-registration/start endpoint to return the AccountRegistrationId in body instead of location --- .../AccountRegistrationsEndpoints.cs | 6 +- .../StartAccountRegistration.cs | 14 +-- .../AccountRegistration.cs | 5 + .../StartAccountRegistrationForm.tsx | 3 +- .../pages/register/-components/actions.ts | 94 +++++++------------ .../shared/lib/api/AccountManagement.Api.json | 22 ++++- .../shared/translations/locale/da-DK.po | 8 -- .../shared/translations/locale/en-US.po | 8 -- .../Identity/StronglyTypedIdTypeConverter.cs | 2 +- 9 files changed, 71 insertions(+), 91 deletions(-) diff --git a/application/account-management/Api/AccountRegistrations/AccountRegistrationsEndpoints.cs b/application/account-management/Api/AccountRegistrations/AccountRegistrationsEndpoints.cs index 5987a4e21..e6d3d0399 100644 --- a/application/account-management/Api/AccountRegistrations/AccountRegistrationsEndpoints.cs +++ b/application/account-management/Api/AccountRegistrations/AccountRegistrationsEndpoints.cs @@ -17,9 +17,9 @@ public void MapEndpoints(IEndpointRouteBuilder routes) => await mediator.Send(query) ).Produces(); - group.MapPost("/start", async Task (StartAccountRegistrationCommand command, ISender mediator) - => (await mediator.Send(command)).AddResourceUri(RoutesPrefix) - ); + group.MapPost("/start", async Task> (StartAccountRegistrationCommand command, ISender mediator) + => await mediator.Send(command) + ).Produces(); group.MapPost("{id}/complete", async Task (AccountRegistrationId id, CompleteAccountRegistrationCommand command, ISender mediator) => await mediator.Send(command with { Id = id }) diff --git a/application/account-management/Application/AccountRegistrations/StartAccountRegistration.cs b/application/account-management/Application/AccountRegistrations/StartAccountRegistration.cs index 9b8238ffb..47660dfde 100644 --- a/application/account-management/Application/AccountRegistrations/StartAccountRegistration.cs +++ b/application/account-management/Application/AccountRegistrations/StartAccountRegistration.cs @@ -12,7 +12,7 @@ namespace PlatformPlatform.AccountManagement.Application.AccountRegistrations; public sealed record StartAccountRegistrationCommand(string Subdomain, string Email) - : ICommand, IRequest> + : ICommand, IRequest> { public TenantId GetTenantId() { @@ -20,6 +20,8 @@ public TenantId GetTenantId() } } +public sealed record StartAccountRegistrationResponse(string AccountRegistrationId, int ValidForSeconds); + public sealed class StartAccountRegistrationValidator : AbstractValidator { public StartAccountRegistrationValidator(ITenantRepository tenantRepository) @@ -40,23 +42,23 @@ public sealed class StartAccountRegistrationCommandHandler( IEmailService emailService, IPasswordHasher passwordHasher, ITelemetryEventsCollector events -) : IRequestHandler> +) : IRequestHandler> { - public async Task> Handle(StartAccountRegistrationCommand command, CancellationToken cancellationToken) + public async Task> Handle(StartAccountRegistrationCommand command, CancellationToken cancellationToken) { var existingAccountRegistrations = accountRegistrationRepository.GetByEmailOrTenantId(command.GetTenantId(), command.Email); if (existingAccountRegistrations.Any(r => !r.HasExpired())) { - return Result.Conflict( + return Result.Conflict( "Account registration for this subdomain/mail has already been started. Please check your spam folder." ); } if (existingAccountRegistrations.Count(r => r.CreatedAt > TimeProvider.System.GetUtcNow().AddDays(-1)) > 3) { - return Result.TooManyRequests("Too many attempts to register this email address. Please try again later."); + return Result.TooManyRequests("Too many attempts to register this email address. Please try again later."); } var oneTimePassword = GenerateOneTimePassword(6); @@ -75,7 +77,7 @@ await emailService.SendAsync(accountRegistration.Email, "Confirm your email addr cancellationToken ); - return accountRegistration.Id; + return new StartAccountRegistrationResponse(accountRegistration.Id, accountRegistration.GetValidForSeconds()); } public static string GenerateOneTimePassword(int length) diff --git a/application/account-management/Domain/AccountRegistrations/AccountRegistration.cs b/application/account-management/Domain/AccountRegistrations/AccountRegistration.cs index 2236ae9f3..c1c24f71e 100644 --- a/application/account-management/Domain/AccountRegistrations/AccountRegistration.cs +++ b/application/account-management/Domain/AccountRegistrations/AccountRegistration.cs @@ -56,6 +56,11 @@ public void MarkAsCompleted() Completed = true; } + + public int GetValidForSeconds() + { + return Convert.ToInt16((ValidUntil - TimeProvider.System.GetUtcNow()).TotalSeconds); + } } [TypeConverter(typeof(StronglyTypedIdTypeConverter))] diff --git a/application/account-management/WebApp/pages/register/-components/StartAccountRegistrationForm.tsx b/application/account-management/WebApp/pages/register/-components/StartAccountRegistrationForm.tsx index 42b0bd293..fddf6cd52 100644 --- a/application/account-management/WebApp/pages/register/-components/StartAccountRegistrationForm.tsx +++ b/application/account-management/WebApp/pages/register/-components/StartAccountRegistrationForm.tsx @@ -1,12 +1,11 @@ import type React from "react"; -import { useState, startTransition } from "react"; +import { startTransition, useActionState, useState } from "react"; import { useFormStatus } from "react-dom"; import { DotIcon } from "lucide-react"; import { Trans } from "@lingui/macro"; import { useLingui } from "@lingui/react"; import { TextField } from "react-aria-components"; import { Navigate } from "@tanstack/react-router"; -import { useActionState } from "react"; import type { State } from "./actions"; import { startAccountRegistration } from "./actions"; import { Button } from "@repo/ui/components/Button"; diff --git a/application/account-management/WebApp/pages/register/-components/actions.ts b/application/account-management/WebApp/pages/register/-components/actions.ts index 5a5ec9049..61265acc8 100644 --- a/application/account-management/WebApp/pages/register/-components/actions.ts +++ b/application/account-management/WebApp/pages/register/-components/actions.ts @@ -1,9 +1,6 @@ -import { i18n } from "@lingui/core"; import { getApiError, getFieldErrors } from "@repo/infrastructure/api/ErrorList"; import { accountManagementApi } from "@/shared/lib/api/client"; -const VALIDATION_LIFETIME = 1000 * 60 * 5; // 5 minutes - interface CurrentRegistration { accountRegistrationId: string; email: string; @@ -37,6 +34,7 @@ export async function startAccountRegistration(_: State, formData: FormData): Pr if (!result.response.ok) { const apiError = getApiError(result); + return { success: false, message: apiError.title, @@ -44,78 +42,50 @@ export async function startAccountRegistration(_: State, formData: FormData): Pr }; } - try { - const location = result.response.headers.get("Location"); - if (!location) { - return { - success: false, - message: i18n.t("An error occured when trying to start Account registration.") - }; - } - const accountRegistrationId = location.split("/").pop(); - if (!accountRegistrationId) { - return { - success: false, - message: i18n.t("An error occured when trying to start Account registration.") - }; - } + if (!result.data) { + throw new Error("Start registration failed."); + } - registration.current = { - accountRegistrationId, - email, - expireAt: new Date(Date.now() + VALIDATION_LIFETIME) - }; + registration.current = { + accountRegistrationId: result.data.accountRegistrationId as string, + email, + expireAt: new Date(Date.now() + (result.data.validForSeconds as number) * 1000) + }; - return { - success: true - }; - } catch (e) { - return { - success: false, - message: i18n.t("An error occured when trying to start Account registration.") - }; - } + return { + success: true + }; } export async function completeAccountRegistration(_: State, formData: FormData): Promise { const oneTimePassword = formData.get("oneTimePassword") as string; - const accountRegistrationId = registration.current?.accountRegistrationId; - if (!accountRegistrationId) { - return { - success: false, - message: i18n.t("An error occured when trying to start Account registration.") - }; + + if (registration.current === undefined) { + throw new Error("Registration not started."); } - try { - const result = await accountManagementApi.POST("/api/account-management/account-registrations/{id}/complete", { - params: { - path: { - id: accountRegistrationId - } - }, - body: { - oneTimePassword + const result = await accountManagementApi.POST("/api/account-management/account-registrations/{id}/complete", { + params: { + path: { + id: registration.current.accountRegistrationId } - }); - - if (!result.response.ok) { - const apiError = getApiError(result); - - return { - success: false, - message: apiError.title, - errors: getFieldErrors(apiError.Errors) - }; + }, + body: { + oneTimePassword } + }); + + if (!result.response.ok) { + const apiError = getApiError(result); - return { - success: true - }; - } catch (e) { return { success: false, - message: i18n.t("An error occured when trying to complete Account registration.") + message: apiError.title, + errors: getFieldErrors(apiError.Errors) }; } + + return { + success: true + }; } diff --git a/application/account-management/WebApp/shared/lib/api/AccountManagement.Api.json b/application/account-management/WebApp/shared/lib/api/AccountManagement.Api.json index 754511ca0..234ee7f0d 100644 --- a/application/account-management/WebApp/shared/lib/api/AccountManagement.Api.json +++ b/application/account-management/WebApp/shared/lib/api/AccountManagement.Api.json @@ -434,7 +434,14 @@ }, "responses": { "200": { - "description": "" + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StartAccountRegistrationResponse" + } + } + } } } } @@ -740,6 +747,19 @@ } } }, + "StartAccountRegistrationResponse": { + "type": "object", + "additionalProperties": false, + "properties": { + "accountRegistrationId": { + "type": "string" + }, + "validForSeconds": { + "type": "integer", + "format": "int32" + } + } + }, "StartAccountRegistrationCommand": { "type": "object", "additionalProperties": false, diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po index 8474859fb..c2d3437ab 100644 --- a/application/account-management/WebApp/shared/translations/locale/da-DK.po +++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po @@ -21,14 +21,6 @@ msgstr "ditnavn@example.com" msgid "Email" msgstr "Email" -#. js-lingui-explicit-id -msgid "An error occured when trying to start Account registration." -msgstr "Der opstod en fejl. Kunne ikke starte kontooprettelse." - -#. js-lingui-explicit-id -msgid "An error occured when trying to complete Account registration." -msgstr "Der opstod en fejl. Kunne ikke færdigøre kontooprettelse." - #. js-lingui-explicit-id msgid "subdomain" msgstr "subdomæne" diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po index ba60b606f..980434b19 100644 --- a/application/account-management/WebApp/shared/translations/locale/en-US.po +++ b/application/account-management/WebApp/shared/translations/locale/en-US.po @@ -21,14 +21,6 @@ msgstr "yourname@example.com" msgid "Email" msgstr "Email" -#. js-lingui-explicit-id -msgid "An error occured when trying to start Account registration." -msgstr "An error occured when trying to start Account registration." - -#. js-lingui-explicit-id -msgid "An error occured when trying to complete Account registration." -msgstr "An error occured when trying to complete Account registration." - #. js-lingui-explicit-id msgid "subdomain" msgstr "subdomain" diff --git a/application/shared-kernel/DomainCore/Identity/StronglyTypedIdTypeConverter.cs b/application/shared-kernel/DomainCore/Identity/StronglyTypedIdTypeConverter.cs index 0a65ac82f..ad91556ba 100644 --- a/application/shared-kernel/DomainCore/Identity/StronglyTypedIdTypeConverter.cs +++ b/application/shared-kernel/DomainCore/Identity/StronglyTypedIdTypeConverter.cs @@ -2,7 +2,7 @@ namespace PlatformPlatform.SharedKernel.DomainCore.Identity; -public abstract class StronglyTypedIdTypeConverter : TypeConverter +public class StronglyTypedIdTypeConverter : TypeConverter where T : StronglyTypedId where TValue : IComparable { private static readonly MethodInfo? TryParseMethod = typeof(T).GetMethod("TryParse");