Skip to content
Closed
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
13 changes: 13 additions & 0 deletions e2e/landing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,16 @@ test('landing page renders without a database', async ({ page }) => {
await expect(page.getByText('Personvern er hele poenget')).toBeVisible();
await expect(page.getByRole('link', { name: 'Logg inn som administrator' })).toBeVisible();
});

test('?lang=en renders the English strings and links to /om', async ({ page }) => {
await page.goto('/?lang=en');
await expect(page.getByText('Privacy is the whole point')).toBeVisible();

await page.getByRole('link', { name: /Read more about how attester.no works/ }).click();
await expect(page.getByRole('heading', { name: 'How attester.no works' })).toBeVisible();
await expect(page.getByText('This is NEVER stored after issuance:')).toBeVisible();

// The toggle swaps back to Norwegian on the same page.
await page.getByRole('link', { name: 'Norsk' }).click();
await expect(page.getByRole('heading', { name: 'Slik fungerer attester.no' })).toBeVisible();
});
19 changes: 12 additions & 7 deletions src/app/login/glemt/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ export const runtime = 'edge';

import React, { useState } from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { Box, Button, CircularProgress, Container, TextField, Typography } from '@mui/material';
import { requestPasswordReset } from '@/util/auth';
import { useToast } from '@/components/ToastProvider';
import { getStrings } from '@/strings';

const ForgotPasswordPage: React.FC = () => {
const [email, setEmail] = useState('');
const [busy, setBusy] = useState(false);
const [sent, setSent] = useState(false);
const searchParams = useSearchParams();
const lang = searchParams.get('lang');
const s = getStrings(lang).auth;
const withLang = (path: string) => (lang === 'en' ? `${path}?lang=en` : path);
const toast = useToast();

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
Expand All @@ -22,7 +28,7 @@ const ForgotPasswordPage: React.FC = () => {
setSent(true);
} catch (error) {
console.error(error);
toast.error((error as Error).message ?? 'Noe gikk galt');
toast.error((error as Error).message ?? s.genericError);
} finally {
setBusy(false);
}
Expand All @@ -32,12 +38,11 @@ const ForgotPasswordPage: React.FC = () => {
<Container component="main" maxWidth="xs">
<Box sx={{ mt: 8, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Typography component="h1" variant="h5" gutterBottom>
Glemt passord
{s.forgotTitle}
</Typography>
{sent ? (
<Typography variant="body1" sx={{ mt: 2 }}>
Hvis kontoen finnes, er det sendt en e-post med lenke for å
sette nytt passord. Sjekk innboksen din.
{s.forgotSent}
</Typography>
) : (
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1, width: '100%' }}>
Expand All @@ -46,7 +51,7 @@ const ForgotPasswordPage: React.FC = () => {
margin="normal"
required
fullWidth
label="E-post"
label={s.email}
type="email"
autoComplete="email"
autoFocus
Expand All @@ -61,11 +66,11 @@ const ForgotPasswordPage: React.FC = () => {
sx={{ mt: 3, mb: 2 }}
disabled={busy || !email}
>
{busy ? <CircularProgress size={20} /> : 'Send lenke'}
{busy ? <CircularProgress size={20} /> : s.forgotSend}
</Button>
</Box>
)}
<Link href="/login">Tilbake til innlogging</Link>
<Link href={withLang('/login')}>{s.backToLogin}</Link>
</Box>
</Container>
);
Expand Down
29 changes: 19 additions & 10 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import { login, useAuth } from '@/util/auth';
import { Box, Button, CircularProgress, Container, TextField, Typography } from '@mui/material';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { useToast } from '@/components/ToastProvider';
import { getStrings } from '@/strings';
import LanguageToggle from '@/components/LanguageToggle';

const LoginPage: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [busy, setBusy] = useState(false);
const currentUser = useAuth();
const router = useRouter();
const searchParams = useSearchParams();
const lang = searchParams.get('lang');
const s = getStrings(lang).auth;
const withLang = (path: string) => (lang === 'en' ? `${path}?lang=en` : path);
const toast = useToast();

useEffect(() => {
Expand All @@ -29,7 +35,7 @@ const LoginPage: React.FC = () => {
router.push('/login/adminpage');
} catch (error) {
console.error('Login failed:', error);
const msg = (error as { message?: string }).message ?? 'Innlogging feilet';
const msg = (error as { message?: string }).message ?? s.loginFailed;
toast.error(msg);
} finally {
setBusy(false);
Expand All @@ -47,15 +53,15 @@ const LoginPage: React.FC = () => {
}}
>
<Typography component="h1" variant="h5">
Logg inn
{s.loginTitle}
</Typography>
<Box component="form" onSubmit={handleLogin} sx={{ mt: 1, width: '100%' }}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
label="E-post"
label={s.email}
autoComplete="email"
autoFocus
value={email}
Expand All @@ -67,7 +73,7 @@ const LoginPage: React.FC = () => {
margin="normal"
required
fullWidth
label="Passord"
label={s.password}
type="password"
autoComplete="current-password"
value={password}
Expand All @@ -81,16 +87,19 @@ const LoginPage: React.FC = () => {
sx={{ mt: 3, mb: 2 }}
disabled={busy || !email || !password}
>
{busy ? <CircularProgress size={20} /> : 'Logg Inn'}
{busy ? <CircularProgress size={20} /> : s.loginButton}
</Button>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Link href="/login/glemt" style={{ fontSize: '0.875rem' }}>
Glemt passord?
<Link href={withLang('/login/glemt')} style={{ fontSize: '0.875rem' }}>
{s.forgotPassword}
</Link>
<Link href="/registrer" style={{ fontSize: '0.875rem' }}>
Registrer ny konto
<Link href={withLang('/registrer')} style={{ fontSize: '0.875rem' }}>
{s.registerLink}
</Link>
</Box>
<Box sx={{ mt: 2, textAlign: 'center' }}>
<LanguageToggle />
</Box>
</Box>
</Box>
</Container>
Expand Down
27 changes: 14 additions & 13 deletions src/app/login/reset/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import { useRouter, useSearchParams } from 'next/navigation';
import { Box, Button, CircularProgress, Container, TextField, Typography } from '@mui/material';
import { completePasswordReset } from '@/util/auth';
import { useToast } from '@/components/ToastProvider';
import { getStrings } from '@/strings';

const ResetPasswordPage: React.FC = () => {
const searchParams = useSearchParams();
const refreshToken = searchParams.get('refreshToken');
const lang = searchParams.get('lang');
const s = getStrings(lang).auth;
const withLang = (path: string) => (lang === 'en' ? `${path}?lang=en` : path);
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [busy, setBusy] = useState(false);
Expand All @@ -21,11 +25,11 @@ const ResetPasswordPage: React.FC = () => {
return (
<Container component="main" maxWidth="xs">
<Box sx={{ mt: 8, textAlign: 'center' }}>
<Typography variant="h5" gutterBottom>Ugyldig lenke</Typography>
<Typography variant="h5" gutterBottom>{s.resetInvalidTitle}</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>
Lenken mangler eller er utløpt. Be om en ny.
{s.resetInvalidBody}
</Typography>
<Link href="/login/glemt">Glemt passord</Link>
<Link href={withLang('/login/glemt')}>{s.forgotTitle}</Link>
</Box>
</Container>
);
Expand All @@ -35,20 +39,17 @@ const ResetPasswordPage: React.FC = () => {
e.preventDefault();
if (busy) return;
if (password !== confirm) {
toast.error('Passordene er ikke like');
toast.error(s.passwordMismatch);
return;
}
setBusy(true);
try {
await completePasswordReset(refreshToken, password);
toast.success('Passordet er endret. Logg inn med det nye passordet.');
toast.success(s.resetDone);
router.push('/login');
} catch (error) {
console.error(error);
toast.error(
((error as Error).message ?? 'Noe gikk galt')
+ '. Lenken kan være utløpt – be om en ny under «Glemt passord».',
);
toast.error(((error as Error).message ?? s.genericError) + s.resetFailedHint);
setBusy(false);
}
};
Expand All @@ -57,15 +58,15 @@ const ResetPasswordPage: React.FC = () => {
<Container component="main" maxWidth="xs">
<Box sx={{ mt: 8, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Typography component="h1" variant="h5">
Sett nytt passord
{s.resetTitle}
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1, width: '100%' }}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
label="Nytt passord"
label={s.newPassword}
type="password"
autoComplete="new-password"
autoFocus
Expand All @@ -78,7 +79,7 @@ const ResetPasswordPage: React.FC = () => {
margin="normal"
required
fullWidth
label="Gjenta nytt passord"
label={s.repeatNewPassword}
type="password"
autoComplete="new-password"
value={confirm}
Expand All @@ -92,7 +93,7 @@ const ResetPasswordPage: React.FC = () => {
sx={{ mt: 3, mb: 2 }}
disabled={busy || !password || !confirm}
>
{busy ? <CircularProgress size={20} /> : 'Endre passord'}
{busy ? <CircularProgress size={20} /> : s.resetButton}
</Button>
</Box>
</Box>
Expand Down
103 changes: 103 additions & 0 deletions src/app/om/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import Link from 'next/link';
import {
Box, Container, Divider, Grid, Paper, Stack, Typography,
} from '@mui/material';
import { getStrings } from '@/strings';
import LanguageToggle from '@/components/LanguageToggle';

export const runtime = 'edge';

export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<{ lang?: string }>;
}) {
const { lang } = await searchParams;
return { title: getStrings(lang).about.metaTitle };
}

export default async function AboutPage({
searchParams,
}: {
searchParams: Promise<{ lang?: string }>;
}) {
const { lang } = await searchParams;
const s = getStrings(lang).about;
const withLang = (path: string) => (lang === 'en' ? `${path}?lang=en` : path);

return (
<Container maxWidth="md" sx={{ py: 6 }}>
<Stack spacing={4}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', gap: 2 }}>
<Typography variant="h3" component="h1">{s.title}</Typography>
<LanguageToggle />
</Box>

<Typography variant="h6" component="p" color="text.secondary">
{s.intro}
</Typography>

<Box>
<Typography variant="h5" gutterBottom>{s.flowTitle}</Typography>
<Typography component="ol" variant="body1" sx={{ pl: 3, '& li': { mb: 1 } }}>
{s.flowSteps.map((step) => <li key={step}>{step}</li>)}
</Typography>
</Box>

<Box>
<Typography variant="h5" gutterBottom>{s.storedTitle}</Typography>
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 6 }}>
<Paper elevation={0} sx={{ p: 3, height: '100%', bgcolor: 'success.light' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }} gutterBottom>
{s.storedIntro}
</Typography>
<Typography component="ul" variant="body2" sx={{ pl: 3, '& li': { mb: 0.5 } }}>
{s.storedItems.map((item) => <li key={item}>{item}</li>)}
</Typography>
</Paper>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<Paper elevation={0} sx={{ p: 3, height: '100%', bgcolor: 'grey.100' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }} gutterBottom>
{s.neverStoredIntro}
</Typography>
<Typography component="ul" variant="body2" sx={{ pl: 3, '& li': { mb: 0.5 } }}>
{s.neverStoredItems.map((item) => <li key={item}>{item}</li>)}
</Typography>
</Paper>
</Grid>
</Grid>
</Box>

<Box>
<Typography variant="h5" gutterBottom>{s.hashTitle}</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>{s.hashBody}</Typography>
<Paper elevation={0} sx={{ p: 3, bgcolor: 'grey.50' }}>
<Typography variant="body1">{s.hashConsequence}</Typography>
</Paper>
</Box>

<Box>
<Typography variant="h5" gutterBottom>{s.verifyTitle}</Typography>
<Typography component="ol" variant="body1" sx={{ pl: 3, '& li': { mb: 1 } }}>
{s.verifySteps.map((step) => <li key={step}>{step}</li>)}
</Typography>
</Box>

<Divider />

<Box>
<Typography variant="h6" gutterBottom>{s.contactTitle}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{s.contactBody}{' '}
<Link href="https://github.com/ArneeMe/attester.no" target="_blank" rel="noreferrer">
GitHub ↗
</Link>
</Typography>
<Link href={withLang('/')}>{s.backToFront}</Link>
</Box>
</Stack>
</Container>
);
}
Loading
Loading