Skip to content

Commit

Permalink
Make account-registration/start endpoint to return the AccountRegistr…
Browse files Browse the repository at this point in the history
…ationId in body instead of location
  • Loading branch information
tjementum committed Jul 22, 2024
1 parent 4d46319 commit 55f921b
Show file tree
Hide file tree
Showing 9 changed files with 71 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
=> await mediator.Send(query)
).Produces<bool>();

group.MapPost("/start", async Task<ApiResult> (StartAccountRegistrationCommand command, ISender mediator)
=> (await mediator.Send(command)).AddResourceUri(RoutesPrefix)
);
group.MapPost("/start", async Task<ApiResult<StartAccountRegistrationResponse>> (StartAccountRegistrationCommand command, ISender mediator)
=> await mediator.Send(command)
).Produces<StartAccountRegistrationResponse>();

group.MapPost("{id}/complete", async Task<ApiResult> (AccountRegistrationId id, CompleteAccountRegistrationCommand command, ISender mediator)
=> await mediator.Send(command with { Id = id })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
namespace PlatformPlatform.AccountManagement.Application.AccountRegistrations;

public sealed record StartAccountRegistrationCommand(string Subdomain, string Email)
: ICommand, IRequest<Result<AccountRegistrationId>>
: ICommand, IRequest<Result<StartAccountRegistrationResponse>>
{
public TenantId GetTenantId()
{
return new TenantId(Subdomain);
}
}

public sealed record StartAccountRegistrationResponse(string AccountRegistrationId, int ValidForSeconds);

public sealed class StartAccountRegistrationValidator : AbstractValidator<StartAccountRegistrationCommand>
{
public StartAccountRegistrationValidator(ITenantRepository tenantRepository)
Expand All @@ -40,23 +42,23 @@ public sealed class StartAccountRegistrationCommandHandler(
IEmailService emailService,
IPasswordHasher<object> passwordHasher,
ITelemetryEventsCollector events
) : IRequestHandler<StartAccountRegistrationCommand, Result<AccountRegistrationId>>
) : IRequestHandler<StartAccountRegistrationCommand, Result<StartAccountRegistrationResponse>>
{
public async Task<Result<AccountRegistrationId>> Handle(StartAccountRegistrationCommand command, CancellationToken cancellationToken)
public async Task<Result<StartAccountRegistrationResponse>> Handle(StartAccountRegistrationCommand command, CancellationToken cancellationToken)
{
var existingAccountRegistrations
= accountRegistrationRepository.GetByEmailOrTenantId(command.GetTenantId(), command.Email);

if (existingAccountRegistrations.Any(r => !r.HasExpired()))

Check warning on line 52 in application/account-management/Application/AccountRegistrations/StartAccountRegistration.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Collection-specific "Exists" method should be used instead of the "Any" extension. (https://rules.sonarsource.com/csharp/RSPEC-6605)

Check warning on line 52 in application/account-management/Application/AccountRegistrations/StartAccountRegistration.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Collection-specific "Exists" method should be used instead of the "Any" extension. (https://rules.sonarsource.com/csharp/RSPEC-6605)

Check warning on line 52 in application/account-management/Application/AccountRegistrations/StartAccountRegistration.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Collection-specific "Exists" method should be used instead of the "Any" extension. (https://rules.sonarsource.com/csharp/RSPEC-6605)

Check warning on line 52 in application/account-management/Application/AccountRegistrations/StartAccountRegistration.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Collection-specific "Exists" method should be used instead of the "Any" extension. (https://rules.sonarsource.com/csharp/RSPEC-6605)
{
return Result<AccountRegistrationId>.Conflict(
return Result<StartAccountRegistrationResponse>.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<AccountRegistrationId>.TooManyRequests("Too many attempts to register this email address. Please try again later.");
return Result<StartAccountRegistrationResponse>.TooManyRequests("Too many attempts to register this email address. Please try again later.");
}

var oneTimePassword = GenerateOneTimePassword(6);
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ public void MarkAsCompleted()

Completed = true;
}

public int GetValidForSeconds()
{
return Convert.ToInt16((ValidUntil - TimeProvider.System.GetUtcNow()).TotalSeconds);
}
}

[TypeConverter(typeof(StronglyTypedIdTypeConverter<string, AccountRegistrationId>))]
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -37,85 +34,58 @@ export async function startAccountRegistration(_: State, formData: FormData): Pr

if (!result.response.ok) {
const apiError = getApiError(result);

return {
success: false,
message: apiError.title,
errors: getFieldErrors(apiError.Errors)
};
}

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<State> {
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
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,14 @@
},
"responses": {
"200": {
"description": ""
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StartAccountRegistrationResponse"
}
}
}
}
}
}
Expand Down Expand Up @@ -740,6 +747,19 @@
}
}
},
"StartAccountRegistrationResponse": {
"type": "object",
"additionalProperties": false,
"properties": {
"accountRegistrationId": {
"type": "string"
},
"validForSeconds": {
"type": "integer",
"format": "int32"
}
}
},
"StartAccountRegistrationCommand": {
"type": "object",
"additionalProperties": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ msgstr "[email protected]"
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ msgstr "[email protected]"
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace PlatformPlatform.SharedKernel.DomainCore.Identity;

public abstract class StronglyTypedIdTypeConverter<TValue, T> : TypeConverter
public class StronglyTypedIdTypeConverter<TValue, T> : TypeConverter
where T : StronglyTypedId<TValue, T> where TValue : IComparable<TValue>
{
private static readonly MethodInfo? TryParseMethod = typeof(T).GetMethod("TryParse");
Expand Down

0 comments on commit 55f921b

Please sign in to comment.