Skip to content

Commit c75363d

Browse files
fix: improve auth (#519)
* fix: improve auth fix: verify email params issue in update email flow fix: send email if email already used fix: auth flows * fix: remove useless function wrapper and add missing word in translations --------- Co-authored-by: Renan Decamps <[email protected]>
1 parent 76deb85 commit c75363d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+451
-435
lines changed

.env.example

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ NEXT_PUBLIC_IS_DEMO="false"
1616
# DATABASE
1717
DATABASE_URL="postgres://${DOCKER_DATABASE_USERNAME}:${DOCKER_DATABASE_PASSWORD}@localhost:${DOCKER_DATABASE_PORT}/${DOCKER_DATABASE_NAME}"
1818

19-
2019
# EMAILS
2120
EMAIL_SERVER="smtp://username:[email protected]:1025"
2221
EMAIL_FROM="Start UI <[email protected]>"

CODE_OF_CONDUCT.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ decisions when appropriate.
5252

5353
This Code of Conduct applies within all community spaces, and also applies when
5454
an individual is officially representing the community in public spaces.
55-
Examples of representing our community include using an official e-mail address,
55+
Examples of representing our community include using an official email address,
5656
posting via an official social media account, or acting as an appointed
5757
representative at an online or offline event.
5858

e2e/admin.spec.ts

-4
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@ import { expect, test } from '@playwright/test';
22
import { pageUtils } from 'e2e/utils/pageUtils';
33
import { ADMIN_EMAIL } from 'e2e/utils/users';
44

5-
import { env } from '@/env.mjs';
65
import { ROUTES_ADMIN } from '@/features/admin/routes';
76

87
test.describe('Admin access', () => {
98
test(`Redirect to Admin (${ROUTES_ADMIN.root()})`, async ({ page }) => {
109
const utils = pageUtils(page);
1110

1211
await utils.loginAdmin({ email: ADMIN_EMAIL });
13-
await page.waitForURL(
14-
`${env.NEXT_PUBLIC_BASE_URL}${ROUTES_ADMIN.root()}**`
15-
);
1612

1713
await expect(page.getByTestId('admin-layout')).toBeVisible();
1814
});

e2e/register.spec.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import locales from '@/locales';
88

99
test.describe('Register flow', () => {
1010
test('Success flow', async ({ page }) => {
11-
await page.goto(ROUTES_AUTH.app.register());
12-
await page.waitForURL(`**${ROUTES_AUTH.app.register()}`);
11+
await page.goto(ROUTES_AUTH.register());
12+
await page.waitForURL(`**${ROUTES_AUTH.register()}`);
1313

1414
await page.getByLabel('Name').fill('Test user');
1515
const email = await getRandomEmail();
@@ -18,24 +18,24 @@ test.describe('Register flow', () => {
1818
.getByRole('button', { name: locales.en.auth.register.actions.create })
1919
.click();
2020

21-
await page.waitForURL(`**${ROUTES_AUTH.app.register()}/**`);
21+
await page.waitForURL(`**${ROUTES_AUTH.register()}/**`);
2222
await page.getByText('Verification code').fill(VALIDATION_CODE_MOCKED);
2323
await expect(
2424
page.getByText(locales.en.auth.data.verificationCode.unknown)
2525
).not.toBeVisible();
2626
});
2727

2828
test('Register with existing email', async ({ page }) => {
29-
await page.goto(ROUTES_AUTH.app.register());
30-
await page.waitForURL(`**${ROUTES_AUTH.app.register()}`);
29+
await page.goto(ROUTES_AUTH.register());
30+
await page.waitForURL(`**${ROUTES_AUTH.register()}`);
3131

3232
await page.getByLabel('Name').fill('Test user');
3333
await page.getByLabel('Email').fill(USER_EMAIL);
3434
await page
3535
.getByRole('button', { name: locales.en.auth.register.actions.create })
3636
.click();
3737

38-
await page.waitForURL(`**${ROUTES_AUTH.app.register()}/**`);
38+
await page.waitForURL(`**${ROUTES_AUTH.register()}/**`);
3939
await page.getByText('Verification code').fill(VALIDATION_CODE_MOCKED);
4040
await expect(
4141
page.getByText(locales.en.auth.data.verificationCode.unknown)
@@ -44,8 +44,8 @@ test.describe('Register flow', () => {
4444

4545
test('Login with a not verified account', async ({ page }) => {
4646
const utils = pageUtils(page);
47-
await page.goto(ROUTES_AUTH.app.register());
48-
await page.waitForURL(`**${ROUTES_AUTH.app.register()}`);
47+
await page.goto(ROUTES_AUTH.register());
48+
await page.waitForURL(`**${ROUTES_AUTH.register()}`);
4949

5050
const email = await getRandomEmail();
5151

e2e/utils/pageUtils.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Page } from '@playwright/test';
22

33
import { ROUTES_AUTH } from '@/features/auth/routes';
44
import { VALIDATION_CODE_MOCKED } from '@/features/auth/utils';
5-
import type { RouterInputs } from '@/lib/trpc/types';
65
import locales from '@/locales';
76

87
/**
@@ -25,9 +24,9 @@ export const pageUtils = (page: Page) => {
2524
/**
2625
* Utility used to authenticate a user on the app
2726
*/
28-
async loginApp(input: RouterInputs['auth']['login'] & { code?: string }) {
29-
await page.goto(ROUTES_AUTH.app.login());
30-
await page.waitForURL(`**${ROUTES_AUTH.app.login()}`);
27+
async loginApp(input: { email: string; code?: string }) {
28+
await page.goto(ROUTES_AUTH.login());
29+
await page.waitForURL(`**${ROUTES_AUTH.login()}`);
3130

3231
await page
3332
.getByPlaceholder(locales.en.auth.data.email.label)
@@ -36,7 +35,7 @@ export const pageUtils = (page: Page) => {
3635
.getByRole('button', { name: locales.en.auth.login.actions.login })
3736
.click();
3837

39-
await page.waitForURL(`**${ROUTES_AUTH.app.login()}/**`);
38+
await page.waitForURL(`**${ROUTES_AUTH.login()}/**`);
4039
await page
4140
.getByText('Verification code')
4241
.fill(input.code ?? VALIDATION_CODE_MOCKED);
@@ -45,9 +44,9 @@ export const pageUtils = (page: Page) => {
4544
/**
4645
* Utility used to authenticate an admin on the app
4746
*/
48-
async loginAdmin(input: RouterInputs['auth']['login'] & { code?: string }) {
49-
await page.goto(ROUTES_AUTH.admin.login());
50-
await page.waitForURL(`**${ROUTES_AUTH.admin.login()}`);
47+
async loginAdmin(input: { email: string; code?: string }) {
48+
await page.goto(ROUTES_AUTH.login());
49+
await page.waitForURL(`**${ROUTES_AUTH.login()}`);
5150

5251
await page
5352
.getByPlaceholder(locales.en.auth.data.email.label)
@@ -56,7 +55,7 @@ export const pageUtils = (page: Page) => {
5655
.getByRole('button', { name: locales.en.auth.login.actions.login })
5756
.click();
5857

59-
await page.waitForURL(`**${ROUTES_AUTH.admin.login()}/**`);
58+
await page.waitForURL(`**${ROUTES_AUTH.login()}/**`);
6059
await page
6160
.getByText('Verification code')
6261
.fill(input.code ?? VALIDATION_CODE_MOCKED);

pnpm-lock.yaml

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prisma/schema/user.prisma

+14-13
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
enum AccountStatus {
2-
// User has registered and verified their email
2+
// User has registered and verified
33
ENABLED
44
// User has been disabled and can't login anymore
55
DISABLED
6-
// User did register, but has not verified their email yet.
6+
// User did register, but has not been verified
77
NOT_VERIFIED
88
}
99

@@ -13,15 +13,16 @@ enum UserRole {
1313
}
1414

1515
model User {
16-
id String @id @default(cuid())
17-
createdAt DateTime @default(now())
18-
updatedAt DateTime @updatedAt
19-
name String?
20-
email String @unique
21-
accountStatus AccountStatus @default(NOT_VERIFIED)
22-
image String?
23-
authorizations UserRole[] @default([APP])
24-
language String @default("en")
25-
lastLoginAt DateTime?
26-
session Session[]
16+
id String @id @default(cuid())
17+
createdAt DateTime @default(now())
18+
updatedAt DateTime @updatedAt
19+
name String?
20+
email String? @unique
21+
isEmailVerified Boolean @default(false)
22+
accountStatus AccountStatus @default(NOT_VERIFIED)
23+
image String?
24+
authorizations UserRole[] @default([APP])
25+
language String @default("en")
26+
lastLoginAt DateTime?
27+
session Session[]
2728
}

src/app/admin/(authenticated)/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function AuthenticatedLayout({
1313
<Suspense>
1414
<GuardAuthenticated
1515
authorizations={['ADMIN']}
16-
loginPath={ROUTES_AUTH.admin.login()}
16+
loginPath={ROUTES_AUTH.login()}
1717
>
1818
<AdminLayout>{children}</AdminLayout>
1919
</GuardAuthenticated>

src/app/admin/(public-only)/layout.tsx

-14
This file was deleted.

src/app/admin/(public-only)/login/[token]/page.tsx

-13
This file was deleted.

src/app/admin/(public-only)/login/page.tsx

-13
This file was deleted.

src/app/app/(authenticated)/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function AuthenticatedLayout({
1313
<Suspense>
1414
<GuardAuthenticated
1515
authorizations={['APP']}
16-
loginPath={ROUTES_AUTH.app.login()}
16+
loginPath={ROUTES_AUTH.login()}
1717
>
1818
<AppLayout>{children}</AppLayout>
1919
</GuardAuthenticated>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Container, Heading, Section, Text } from '@react-email/components';
2+
3+
import { Footer } from '@/emails/components/Footer';
4+
import { Layout } from '@/emails/components/Layout';
5+
import { styles } from '@/emails/styles';
6+
import i18n from '@/lib/i18n/server';
7+
8+
type EmailUpdateAlreadyUsedProps = {
9+
language: string;
10+
name: string;
11+
email: string;
12+
};
13+
14+
export const EmailUpdateAlreadyUsed = ({
15+
language,
16+
name,
17+
email,
18+
}: EmailUpdateAlreadyUsedProps) => {
19+
i18n.changeLanguage(language);
20+
return (
21+
<Layout
22+
preview={i18n.t('emails:emailUpdateAlreadyUsed.preview')}
23+
language={language}
24+
>
25+
<Container style={styles.container}>
26+
<Heading style={styles.h1}>
27+
{i18n.t('emails:emailUpdateAlreadyUsed.title')}
28+
</Heading>
29+
<Section style={styles.section}>
30+
<Text style={styles.text}>
31+
{i18n.t('emails:emailUpdateAlreadyUsed.hello', {
32+
name,
33+
})}
34+
<br />
35+
{i18n.t('emails:emailUpdateAlreadyUsed.intro', { email })}
36+
</Text>
37+
<Text style={styles.textMuted}>
38+
{i18n.t('emails:emailUpdateAlreadyUsed.ignoreHelper')}
39+
</Text>
40+
</Section>
41+
<Footer />
42+
</Container>
43+
</Layout>
44+
);
45+
};
46+
47+
export default EmailUpdateAlreadyUsed;

src/emails/templates/email-address-change.tsx renamed to src/emails/templates/email-update-code.tsx

+10-13
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,37 @@ import { styles } from '@/emails/styles';
66
import { VALIDATION_TOKEN_EXPIRATION_IN_MINUTES } from '@/features/auth/utils';
77
import i18n from '@/lib/i18n/server';
88

9-
type EmailAddressChangeProps = {
9+
type EmailUpdateCodeProps = {
1010
language: string;
1111
name: string;
1212
code: string;
1313
};
1414

15-
export const EmailAddressChange = ({
15+
export const EmailUpdateCode = ({
1616
language,
1717
name,
1818
code,
19-
}: EmailAddressChangeProps) => {
19+
}: EmailUpdateCodeProps) => {
2020
i18n.changeLanguage(language);
2121
return (
22-
<Layout
23-
preview={i18n.t('emails:emailAddressChange.preview')}
24-
language={language}
25-
>
22+
<Layout preview={i18n.t('emails:emailUpdate.preview')} language={language}>
2623
<Container style={styles.container}>
2724
<Heading style={styles.h1}>
28-
{i18n.t('emails:emailAddressChange.title')}
25+
{i18n.t('emails:emailUpdate.title')}
2926
</Heading>
3027
<Section style={styles.section}>
3128
<Text style={styles.text}>
32-
{i18n.t('emails:emailAddressChange.hello', { name: name ?? '' })}
29+
{i18n.t('emails:emailUpdate.hello', { name: name ?? '' })}
3330
<br />
34-
{i18n.t('emails:emailAddressChange.intro')}
31+
{i18n.t('emails:emailUpdate.intro')}
3532
</Text>
3633
<Text style={styles.code}>{code}</Text>
3734
<Text style={styles.textMuted}>
38-
{i18n.t('emails:emailAddressChange.validityTime', {
35+
{i18n.t('emails:emailUpdate.validityTime', {
3936
expiration: VALIDATION_TOKEN_EXPIRATION_IN_MINUTES,
4037
})}
4138
<br />
42-
{i18n.t('emails:emailAddressChange.ignoreHelper')}
39+
{i18n.t('emails:emailUpdate.ignoreHelper')}
4340
</Text>
4441
</Section>
4542
<Footer />
@@ -48,4 +45,4 @@ export const EmailAddressChange = ({
4845
);
4946
};
5047

51-
export default EmailAddressChange;
48+
export default EmailUpdateCode;

0 commit comments

Comments
 (0)