Skip to content

Commit 66c16d0

Browse files
committed
Add logic to prevent inviting users if account lacks a name
1 parent b146e86 commit 66c16d0

File tree

8 files changed

+149
-6
lines changed

8 files changed

+149
-6
lines changed

application/account-management/Core/Features/Users/Commands/InviteUser.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using FluentValidation;
22
using JetBrains.Annotations;
3+
using PlatformPlatform.AccountManagement.Features.Tenants.Domain;
34
using PlatformPlatform.AccountManagement.Features.Users.Domain;
45
using PlatformPlatform.SharedKernel.Cqrs;
56
using PlatformPlatform.SharedKernel.ExecutionContext;
@@ -26,6 +27,7 @@ public InviteUserValidator()
2627

2728
public sealed class InviteUserHandler(
2829
IUserRepository userRepository,
30+
ITenantRepository tenantRepository,
2931
IEmailClient emailClient,
3032
IExecutionContext executionContext,
3133
IMediator mediator,
@@ -39,6 +41,12 @@ public async Task<Result> Handle(InviteUserCommand command, CancellationToken ca
3941
return Result.Forbidden("Only owners are allowed to invite other users.");
4042
}
4143

44+
var tenant = await tenantRepository.GetCurrentTenantAsync(cancellationToken);
45+
if (string.IsNullOrWhiteSpace(tenant.Name))
46+
{
47+
return Result.BadRequest("Account name must be set before inviting users.");
48+
}
49+
4250
if (!await userRepository.IsEmailFreeAsync(command.Email, cancellationToken))
4351
{
4452
return Result.BadRequest($"The user with '{command.Email}' already exists.");
@@ -53,7 +61,7 @@ public async Task<Result> Handle(InviteUserCommand command, CancellationToken ca
5361
var loginPath = $"{Environment.GetEnvironmentVariable(SinglePageAppConfiguration.PublicUrlKey)}/login";
5462
var inviter = $"{executionContext.UserInfo.FirstName} {executionContext.UserInfo.LastName}".Trim();
5563
inviter = inviter.Length > 0 ? inviter : executionContext.UserInfo.Email;
56-
await emailClient.SendAsync(command.Email.ToLower(), $"You have been invited to join {executionContext.TenantId} on PlatformPlatform",
64+
await emailClient.SendAsync(command.Email.ToLower(), $"You have been invited to join {tenant.Name} on PlatformPlatform",
5765
$"""
5866
<h1 style="text-align:center;font-family:sans-serif;font-size:20px">
5967
<b>{inviter}</b> invited you to join PlatformPlatform.

application/account-management/Tests/Authentication/CompleteLoginTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ public async Task CompleteLogin_WhenLoginExpired_ShouldReturnBadRequest()
192192
public async Task CompleteLogin_WhenUserInviteCompleted_ShouldTrackUserInviteAcceptedEvent()
193193
{
194194
// Arrange
195+
// Set tenant name first (required for inviting users)
196+
Connection.Update("Tenants", "Id", DatabaseSeeder.Tenant1.Id.ToString(),
197+
[("Name", "Test Company")]
198+
);
199+
195200
var email = Faker.Internet.Email();
196201
var inviteUserCommand = new InviteUserCommand(email);
197202
await AuthenticatedOwnerHttpClient.PostAsJsonAsync("/api/account-management/users/invite", inviteUserCommand);

application/account-management/Tests/Users/InviteUserTests.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,31 @@ namespace PlatformPlatform.AccountManagement.Tests.Users;
1414
public sealed class InviteUserTests : EndpointBaseTest<AccountManagementDbContext>
1515
{
1616
[Fact]
17-
public async Task InviteUser_WhenValid_ShouldCreateUserWithEmailConfirmedFalse()
17+
public async Task InviteUser_WhenTenantNameNotSet_ShouldReturnBadRequest()
1818
{
1919
// Arrange
20-
var tenantId = DatabaseSeeder.Tenant1.Id.ToString();
20+
var email = Faker.Internet.Email();
21+
var command = new InviteUserCommand(email);
22+
23+
// Act
24+
var response = await AuthenticatedOwnerHttpClient.PostAsJsonAsync("/api/account-management/users/invite", command);
25+
26+
// Assert
27+
await response.ShouldHaveErrorStatusCode(HttpStatusCode.BadRequest, "Account name must be set before inviting users.");
28+
29+
TelemetryEventsCollectorSpy.AreAllEventsDispatched.Should().BeFalse();
30+
}
31+
32+
[Fact]
33+
public async Task InviteUser_WhenTenantHasName_ShouldCreateUserAndUseTenantNameInEmail()
34+
{
35+
// Arrange
36+
var tenantName = "Test Company";
37+
// Update tenant name using SqliteConnectionExtensions
38+
Connection.Update("Tenants", "Id", DatabaseSeeder.Tenant1.Id.ToString(),
39+
[("Name", tenantName)]
40+
);
41+
2142
var email = Faker.Internet.Email();
2243
var command = new InviteUserCommand(email);
2344

@@ -26,9 +47,11 @@ public async Task InviteUser_WhenValid_ShouldCreateUserWithEmailConfirmedFalse()
2647

2748
// Assert
2849
await response.ShouldBeSuccessfulPostRequest(hasLocation: false);
50+
51+
// Verify user was created
2952
Connection.ExecuteScalar<long>(
3053
"SELECT COUNT(*) FROM Users WHERE TenantId = @tenantId AND Email = @email AND EmailConfirmed = 0",
31-
new { tenantId, email = email.ToLower() }
54+
new { tenantId = DatabaseSeeder.Tenant1.Id.ToString(), email = email.ToLower() }
3255
).Should().Be(1);
3356

3457
TelemetryEventsCollectorSpy.CollectedEvents.Count.Should().Be(2);
@@ -38,7 +61,7 @@ public async Task InviteUser_WhenValid_ShouldCreateUserWithEmailConfirmedFalse()
3861

3962
await EmailClient.Received(1).SendAsync(
4063
email.ToLower(),
41-
$"You have been invited to join {tenantId} on PlatformPlatform",
64+
$"You have been invited to join {tenantName} on PlatformPlatform",
4265
Arg.Is<string>(s => s.Contains("To gain access")),
4366
Arg.Any<CancellationToken>()
4467
);
@@ -68,6 +91,11 @@ public async Task InviteUser_WhenInvalidEmail_ShouldReturnBadRequest()
6891
public async Task InviteUser_WhenUserExists_ShouldReturnBadRequest()
6992
{
7093
// Arrange
94+
// Set tenant name first (required for inviting users)
95+
Connection.Update("Tenants", "Id", DatabaseSeeder.Tenant1.Id.ToString(),
96+
[("Name", "Test Company")]
97+
);
98+
7199
var existingUserEmail = DatabaseSeeder.Tenant1Owner.Email;
72100
var command = new InviteUserCommand(existingUserEmail);
73101

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { t } from "@lingui/core/macro";
2+
import { Trans } from "@lingui/react/macro";
3+
import { Button } from "@repo/ui/components/Button";
4+
import { Dialog } from "@repo/ui/components/Dialog";
5+
import { DialogContent, DialogFooter, DialogHeader } from "@repo/ui/components/DialogFooter";
6+
import { Heading } from "@repo/ui/components/Heading";
7+
import { Modal } from "@repo/ui/components/Modal";
8+
import { Link } from "@tanstack/react-router";
9+
import { AlertCircleIcon, XIcon } from "lucide-react";
10+
11+
interface TenantNameRequiredDialogProps {
12+
isOpen: boolean;
13+
onOpenChange: (isOpen: boolean) => void;
14+
}
15+
16+
export function TenantNameRequiredDialog({ isOpen, onOpenChange }: Readonly<TenantNameRequiredDialogProps>) {
17+
return (
18+
<Modal isOpen={isOpen} onOpenChange={onOpenChange} isDismissable={true}>
19+
<Dialog className="sm:max-w-lg">
20+
{({ close }) => (
21+
<>
22+
<XIcon onClick={close} className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" />
23+
<DialogHeader description={t`Help your team recognize your invites`}>
24+
<Heading slot="title" className="text-2xl">
25+
<Trans>Add your account name</Trans>
26+
</Heading>
27+
</DialogHeader>
28+
29+
<DialogContent className="flex flex-col gap-4">
30+
<div className="flex items-center gap-3 rounded-lg border border-border bg-muted/50 p-4">
31+
<AlertCircleIcon className="h-5 w-5 text-warning" />
32+
<p className="text-sm">
33+
<Trans>Your team needs to know who's inviting them. Add an account name to get started.</Trans>
34+
</p>
35+
</div>
36+
</DialogContent>
37+
38+
<DialogFooter>
39+
<Button variant="secondary" onPress={close}>
40+
<Trans>Cancel</Trans>
41+
</Button>
42+
<Link to="/admin/account">
43+
<Button variant="primary" onPress={close}>
44+
<Trans>Go to account settings</Trans>
45+
</Button>
46+
</Link>
47+
</DialogFooter>
48+
</>
49+
)}
50+
</Dialog>
51+
</Modal>
52+
);
53+
}

application/account-management/WebApp/routes/admin/users/-components/UserToolbar.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PlusIcon, Trash2Icon } from "lucide-react";
88
import { useState } from "react";
99
import { DeleteUserDialog } from "./DeleteUserDialog";
1010
import InviteUserDialog from "./InviteUserDialog";
11+
import { TenantNameRequiredDialog } from "./TenantNameRequiredDialog";
1112
import { UserQuerying } from "./UserQuerying";
1213

1314
type UserDetails = components["schemas"]["UserDetails"];
@@ -19,28 +20,39 @@ interface UserToolbarProps {
1920

2021
export function UserToolbar({ selectedUsers, onSelectedUsersChange }: Readonly<UserToolbarProps>) {
2122
const { data: currentUser } = api.useQuery("get", "/api/account-management/users/me");
23+
const { data: tenant } = api.useQuery("get", "/api/account-management/tenants/current");
2224
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
2325
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
26+
const [showTenantNameRequiredDialog, setShowTenantNameRequiredDialog] = useState(false);
2427
const [_isFilterBarExpanded, setIsFilterBarExpanded] = useState(false);
2528
const [_hasActiveFilters, setHasActiveFilters] = useState(false);
2629
const [shouldUseCompactButtons, setShouldUseCompactButtons] = useState(false);
2730

2831
const isOwner = currentUser?.role === UserRole.Owner;
2932
const hasSelectedSelf = selectedUsers.some((user) => user.id === currentUser?.id);
33+
const hasTenantName = tenant?.name && tenant.name.trim() !== "";
3034

3135
const handleFilterStateChange = (isExpanded: boolean, hasFilters: boolean, useCompact: boolean) => {
3236
setIsFilterBarExpanded(isExpanded);
3337
setHasActiveFilters(hasFilters);
3438
setShouldUseCompactButtons(useCompact);
3539
};
3640

41+
const handleInviteClick = () => {
42+
if (!hasTenantName) {
43+
setShowTenantNameRequiredDialog(true);
44+
return;
45+
}
46+
setIsInviteModalOpen(true);
47+
};
48+
3749
return (
3850
<div className="-mt-5 mb-4 flex items-center justify-between gap-2 bg-background/95 pt-5 backdrop-blur-sm">
3951
<UserQuerying onFilterStateChange={handleFilterStateChange} onFiltersUpdated={() => onSelectedUsersChange([])} />
4052
<div className="mt-6 flex items-center gap-2">
4153
{selectedUsers.length < 2 && isOwner && (
4254
<TooltipTrigger>
43-
<Button variant="primary" onPress={() => setIsInviteModalOpen(true)} aria-label={t`Invite user`}>
55+
<Button variant="primary" onPress={handleInviteClick} aria-label={t`Invite user`}>
4456
<PlusIcon className="h-5 w-5" />
4557
<span className={shouldUseCompactButtons ? "hidden" : "hidden sm:inline"}>
4658
<Trans>Invite user</Trans>
@@ -75,6 +87,7 @@ export function UserToolbar({ selectedUsers, onSelectedUsersChange }: Readonly<U
7587
)}
7688
</div>
7789
{isOwner && <InviteUserDialog isOpen={isInviteModalOpen} onOpenChange={setIsInviteModalOpen} />}
90+
<TenantNameRequiredDialog isOpen={showTenantNameRequiredDialog} onOpenChange={setShowTenantNameRequiredDialog} />
7891
<DeleteUserDialog
7992
users={selectedUsers}
8093
isOpen={isDeleteModalOpen}

application/account-management/WebApp/shared/translations/locale/da-DK.po

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ msgstr "Tilføj flere i Brugere menuen"
5050
msgid "Add profile picture"
5151
msgstr "Tilføj profilbillede"
5252

53+
msgid "Add your account name"
54+
msgstr "Tilføj dit kontonavn"
55+
5356
msgid "Admin"
5457
msgstr "Admin"
5558

@@ -219,6 +222,12 @@ msgstr "Filtre"
219222
msgid "First name"
220223
msgstr "Fornavn"
221224

225+
msgid "Go to account settings"
226+
msgstr "Gå til kontoindstillinger"
227+
228+
msgid "Help your team recognize your invites"
229+
msgstr "Hjælp dit team med at genkende dine invitationer"
230+
222231
msgid "Here's your overview of what's happening."
223232
msgstr "Her er din oversigt over, hvad der sker."
224233

@@ -503,6 +512,9 @@ msgstr "Velkommen hjem"
503512
msgid "Welcome home, {0}"
504513
msgstr "Velkommen hjem, {0}"
505514

515+
msgid "Your team needs to know who's inviting them. Add an account name to get started."
516+
msgstr "Dit team skal vide, hvem der inviterer dem. Tilføj et kontonavn for at komme i gang."
517+
506518
msgid "Your verification code has expired"
507519
msgstr "Din bekræftelseskode er udløbet"
508520

application/account-management/WebApp/shared/translations/locale/en-US.po

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ msgstr "Add more in the Users menu"
5050
msgid "Add profile picture"
5151
msgstr "Add profile picture"
5252

53+
msgid "Add your account name"
54+
msgstr "Add your account name"
55+
5356
msgid "Admin"
5457
msgstr "Admin"
5558

@@ -219,6 +222,12 @@ msgstr "Filters"
219222
msgid "First name"
220223
msgstr "First name"
221224

225+
msgid "Go to account settings"
226+
msgstr "Go to account settings"
227+
228+
msgid "Help your team recognize your invites"
229+
msgstr "Help your team recognize your invites"
230+
222231
msgid "Here's your overview of what's happening."
223232
msgstr "Here's your overview of what's happening."
224233

@@ -503,6 +512,9 @@ msgstr "Welcome home"
503512
msgid "Welcome home, {0}"
504513
msgstr "Welcome home, {0}"
505514

515+
msgid "Your team needs to know who's inviting them. Add an account name to get started."
516+
msgstr "Your team needs to know who's inviting them. Add an account name to get started."
517+
506518
msgid "Your verification code has expired"
507519
msgstr "Your verification code has expired"
508520

application/account-management/WebApp/shared/translations/locale/nl-NL.po

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ msgstr "Nieuwe toevoegen via het Gebruikers menu"
5050
msgid "Add profile picture"
5151
msgstr "Profielfoto toevoegen"
5252

53+
msgid "Add your account name"
54+
msgstr "Voeg je accountnaam toe"
55+
5356
msgid "Admin"
5457
msgstr "Beheerder"
5558

@@ -219,6 +222,12 @@ msgstr "Filters"
219222
msgid "First name"
220223
msgstr "Voornaam"
221224

225+
msgid "Go to account settings"
226+
msgstr "Ga naar accountinstellingen"
227+
228+
msgid "Help your team recognize your invites"
229+
msgstr "Help je team je uitnodigingen te herkennen"
230+
222231
msgid "Here's your overview of what's happening."
223232
msgstr "Hier is je overzicht van wat er gebeurt."
224233

@@ -503,6 +512,9 @@ msgstr "Welkom home"
503512
msgid "Welcome home, {0}"
504513
msgstr "Welkom home, {0}"
505514

515+
msgid "Your team needs to know who's inviting them. Add an account name to get started."
516+
msgstr "Je team moet weten wie hen uitnodigt. Voeg een accountnaam toe om te beginnen."
517+
506518
msgid "Your verification code has expired"
507519
msgstr "Je verificatiecode is verlopen"
508520

0 commit comments

Comments
 (0)