feat(accounts): outgoing mail over SMTP (Resend), email verification, fail-closed password reset
- mail/ module (lettre, ru/en templates), links built only from PUBLIC_BASE_URL - migration 0006: accounts.email_verified_at, shared email_tokens table (verify + reset), existing accounts marked verified - reset mailer no longer logs tokens; RESET_MAIL_MODE=log is dev-only and refused with COOKIE_SECURE=true; Disabled by default answers 503 - forgot-password and resend-verification do their work in a background task (no timing oracle) - device linking requires a verified email; email_verified exposed via /me and gRPC - gateway rate limits, SMTP_* in compose and .env.example - frontend: verify, forgot-password, reset-password pages, verify banner, ru/en strings
This commit is contained in:
parent
12ce6216b6
commit
c57f851a8b
48 changed files with 1810 additions and 120 deletions
|
|
@ -71,7 +71,12 @@ export function createApiClient(
|
|||
headers.set('content-type', 'application/json')
|
||||
body = JSON.stringify(options.json)
|
||||
}
|
||||
if (accessToken && !path.startsWith('/auth/')) headers.set('authorization', `Bearer ${accessToken}`)
|
||||
// /auth/* stays bearer-free for login/register/refresh/logout, except the
|
||||
// signed-in resend-verification call, which the gateway resolves into an
|
||||
// account identity.
|
||||
const needsBearer =
|
||||
accessToken && (!path.startsWith('/auth/') || path === '/auth/resend-verification')
|
||||
if (needsBearer) headers.set('authorization', `Bearer ${accessToken}`)
|
||||
return fetchImpl(`${baseUrl}${path}`, {
|
||||
method: options.method ?? 'GET',
|
||||
headers,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useTranslation } from '@shared/i18n/react'
|
||||
import { Outlet } from 'react-router'
|
||||
import { VerifyEmailBanner } from '@features/auth/VerifyEmailBanner'
|
||||
import { KonamiEffect } from '../easter/KonamiEffect'
|
||||
import { Footer } from './Footer'
|
||||
import { TopBar } from './topbar/TopBar'
|
||||
|
|
@ -17,6 +18,7 @@ export function AppShell() {
|
|||
<KonamiEffect />
|
||||
<TopBar />
|
||||
<main id="main" className="mx-auto flex w-full max-w-[1120px] flex-1 flex-col px-4 pt-(--topbar-h) sm:px-6">
|
||||
<VerifyEmailBanner />
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ test('a guest never calls /auth/refresh (no 401 in the console)', async () => {
|
|||
test('signed-in visitor sees their nick instead of login', async () => {
|
||||
mockFetch({
|
||||
'POST /auth/refresh': () => json({ access_token: 't' }),
|
||||
'GET /me': () => json({ id: '1', email: 'a@b.c', display_nick: 'Rider', role: 'user', avatar_url: null, created_at: '' }),
|
||||
'GET /me': () => json({ id: '1', email: 'a@b.c', display_nick: 'Rider', role: 'user', avatar_url: null,
|
||||
email_verified: true, created_at: '' }),
|
||||
})
|
||||
renderApp(routes, '/')
|
||||
expect(await screen.findByRole('link', { name: /Rider/ })).toHaveAttribute('href', '/account')
|
||||
|
|
|
|||
|
|
@ -4,5 +4,7 @@ export type Me = {
|
|||
display_nick: string
|
||||
role: 'user' | 'admin'
|
||||
avatar_url: string | null
|
||||
/** False until the address is confirmed; drives the verify-email banner and device linking. */
|
||||
email_verified: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue