Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve auth #519

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ NEXT_PUBLIC_IS_DEMO="false"
# DATABASE
DATABASE_URL="postgres://${DOCKER_DATABASE_USERNAME}:${DOCKER_DATABASE_PASSWORD}@localhost:${DOCKER_DATABASE_PORT}/${DOCKER_DATABASE_NAME}"


# EMAILS
EMAIL_SERVER="smtp://username:[email protected]:1025"
EMAIL_FROM="Start UI <[email protected]>"
Expand Down
2 changes: 1 addition & 1 deletion CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ decisions when appropriate.

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

Expand Down
4 changes: 0 additions & 4 deletions e2e/admin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@ import { expect, test } from '@playwright/test';
import { pageUtils } from 'e2e/utils/pageUtils';
import { ADMIN_EMAIL } from 'e2e/utils/users';

import { env } from '@/env.mjs';
import { ROUTES_ADMIN } from '@/features/admin/routes';

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

await utils.loginAdmin({ email: ADMIN_EMAIL });
await page.waitForURL(
`${env.NEXT_PUBLIC_BASE_URL}${ROUTES_ADMIN.root()}**`
);

await expect(page.getByTestId('admin-layout')).toBeVisible();
});
Expand Down
16 changes: 8 additions & 8 deletions e2e/register.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import locales from '@/locales';

test.describe('Register flow', () => {
test('Success flow', async ({ page }) => {
await page.goto(ROUTES_AUTH.app.register());
await page.waitForURL(`**${ROUTES_AUTH.app.register()}`);
await page.goto(ROUTES_AUTH.register());
await page.waitForURL(`**${ROUTES_AUTH.register()}`);

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

await page.waitForURL(`**${ROUTES_AUTH.app.register()}/**`);
await page.waitForURL(`**${ROUTES_AUTH.register()}/**`);
await page.getByText('Verification code').fill(VALIDATION_CODE_MOCKED);
await expect(
page.getByText(locales.en.auth.data.verificationCode.unknown)
).not.toBeVisible();
});

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

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

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

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

const email = await getRandomEmail();

Expand Down
17 changes: 8 additions & 9 deletions e2e/utils/pageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Page } from '@playwright/test';

import { ROUTES_AUTH } from '@/features/auth/routes';
import { VALIDATION_CODE_MOCKED } from '@/features/auth/utils';
import type { RouterInputs } from '@/lib/trpc/types';
import locales from '@/locales';

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

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

await page.waitForURL(`**${ROUTES_AUTH.app.login()}/**`);
await page.waitForURL(`**${ROUTES_AUTH.login()}/**`);
await page
.getByText('Verification code')
.fill(input.code ?? VALIDATION_CODE_MOCKED);
Expand All @@ -45,9 +44,9 @@ export const pageUtils = (page: Page) => {
/**
* Utility used to authenticate an admin on the app
*/
async loginAdmin(input: RouterInputs['auth']['login'] & { code?: string }) {
await page.goto(ROUTES_AUTH.admin.login());
await page.waitForURL(`**${ROUTES_AUTH.admin.login()}`);
async loginAdmin(input: { email: string; code?: string }) {
await page.goto(ROUTES_AUTH.login());
await page.waitForURL(`**${ROUTES_AUTH.login()}`);

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

await page.waitForURL(`**${ROUTES_AUTH.admin.login()}/**`);
await page.waitForURL(`**${ROUTES_AUTH.login()}/**`);
await page
.getByText('Verification code')
.fill(input.code ?? VALIDATION_CODE_MOCKED);
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 14 additions & 13 deletions prisma/schema/user.prisma
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
enum AccountStatus {
// User has registered and verified their email
// User has registered and verified
ENABLED
// User has been disabled and can't login anymore
DISABLED
// User did register, but has not verified their email yet.
// User did register, but has not been verified
NOT_VERIFIED
}

Expand All @@ -13,15 +13,16 @@ enum UserRole {
}

model User {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String?
email String @unique
accountStatus AccountStatus @default(NOT_VERIFIED)
image String?
authorizations UserRole[] @default([APP])
language String @default("en")
lastLoginAt DateTime?
session Session[]
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String?
email String? @unique
isEmailVerified Boolean @default(false)
accountStatus AccountStatus @default(NOT_VERIFIED)
image String?
authorizations UserRole[] @default([APP])
language String @default("en")
lastLoginAt DateTime?
session Session[]
}
File renamed without changes.
2 changes: 1 addition & 1 deletion src/app/admin/(authenticated)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function AuthenticatedLayout({
<Suspense>
<GuardAuthenticated
authorizations={['ADMIN']}
loginPath={ROUTES_AUTH.admin.login()}
loginPath={ROUTES_AUTH.login()}
>
<AdminLayout>{children}</AdminLayout>
</GuardAuthenticated>
Expand Down
14 changes: 0 additions & 14 deletions src/app/admin/(public-only)/layout.tsx

This file was deleted.

13 changes: 0 additions & 13 deletions src/app/admin/(public-only)/login/[token]/page.tsx

This file was deleted.

13 changes: 0 additions & 13 deletions src/app/admin/(public-only)/login/page.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/app/app/(authenticated)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function AuthenticatedLayout({
<Suspense>
<GuardAuthenticated
authorizations={['APP']}
loginPath={ROUTES_AUTH.app.login()}
loginPath={ROUTES_AUTH.login()}
>
<AppLayout>{children}</AppLayout>
</GuardAuthenticated>
Expand Down
47 changes: 47 additions & 0 deletions src/emails/templates/email-update-already-used.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Container, Heading, Section, Text } from '@react-email/components';

import { Footer } from '@/emails/components/Footer';
import { Layout } from '@/emails/components/Layout';
import { styles } from '@/emails/styles';
import i18n from '@/lib/i18n/server';

type EmailUpdateAlreadyUsedProps = {
language: string;
name: string;
email: string;
};

export const EmailUpdateAlreadyUsed = ({
language,
name,
email,
}: EmailUpdateAlreadyUsedProps) => {
i18n.changeLanguage(language);
return (
<Layout
preview={i18n.t('emails:emailUpdateAlreadyUsed.preview')}
language={language}
>
<Container style={styles.container}>
<Heading style={styles.h1}>
{i18n.t('emails:emailUpdateAlreadyUsed.title')}
</Heading>
<Section style={styles.section}>
<Text style={styles.text}>
{i18n.t('emails:emailUpdateAlreadyUsed.hello', {
name,
})}
<br />
{i18n.t('emails:emailUpdateAlreadyUsed.intro', { email })}
</Text>
<Text style={styles.textMuted}>
{i18n.t('emails:emailUpdateAlreadyUsed.ignoreHelper')}
</Text>
</Section>
<Footer />
</Container>
</Layout>
);
};

export default EmailUpdateAlreadyUsed;
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,37 @@ import { styles } from '@/emails/styles';
import { VALIDATION_TOKEN_EXPIRATION_IN_MINUTES } from '@/features/auth/utils';
import i18n from '@/lib/i18n/server';

type EmailAddressChangeProps = {
type EmailUpdateCodeProps = {
language: string;
name: string;
code: string;
};

export const EmailAddressChange = ({
export const EmailUpdateCode = ({
language,
name,
code,
}: EmailAddressChangeProps) => {
}: EmailUpdateCodeProps) => {
i18n.changeLanguage(language);
return (
<Layout
preview={i18n.t('emails:emailAddressChange.preview')}
language={language}
>
<Layout preview={i18n.t('emails:emailUpdate.preview')} language={language}>
<Container style={styles.container}>
<Heading style={styles.h1}>
{i18n.t('emails:emailAddressChange.title')}
{i18n.t('emails:emailUpdate.title')}
</Heading>
<Section style={styles.section}>
<Text style={styles.text}>
{i18n.t('emails:emailAddressChange.hello', { name: name ?? '' })}
{i18n.t('emails:emailUpdate.hello', { name: name ?? '' })}
<br />
{i18n.t('emails:emailAddressChange.intro')}
{i18n.t('emails:emailUpdate.intro')}
</Text>
<Text style={styles.code}>{code}</Text>
<Text style={styles.textMuted}>
{i18n.t('emails:emailAddressChange.validityTime', {
{i18n.t('emails:emailUpdate.validityTime', {
expiration: VALIDATION_TOKEN_EXPIRATION_IN_MINUTES,
})}
<br />
{i18n.t('emails:emailAddressChange.ignoreHelper')}
{i18n.t('emails:emailUpdate.ignoreHelper')}
</Text>
</Section>
<Footer />
Expand All @@ -48,4 +45,4 @@ export const EmailAddressChange = ({
);
};

export default EmailAddressChange;
export default EmailUpdateCode;
Loading
Loading