feat(dashboard): mobile layout, glass UI, context bottom nav + OIDC PKCE (#1079)

* feat(dashboard): mobile layout, glass tiles, plain-text countdown, place photos

- Rework the mobile dashboard: cover hero, separate boarding-pass card,
  trimmed atlas (trips + days only), stacked widgets
- New floating bottom tab bar with a centred context-aware + button
  (new trip / place / journey / entry depending on the page)
- Move profile + notifications into a small top strip on the dashboard
- Desktop: glassmorphic tiles (light + dark), neutral dark palette,
  plain-text countdown module, real place photos in the boarding pass

* i18n(dashboard): translate new dashboard keys across all locales

Fill the dashboard-rework keys (hero, atlas, fx, tz, upcoming, copy
dialog, aria labels, countdown) that were left as English placeholders,
plus the new startsIn/aria keys, for all 19 languages.

* feat(oidc): send PKCE (S256) in the OIDC login flow

The OIDC client now generates a code_verifier per login, sends the
S256 code_challenge on the authorize request and the code_verifier on
the token exchange. Works whether the provider has PKCE optional or
required (fixes login against providers that require PKCE, e.g. Pocket ID).
This commit is contained in:
Maurice
2026-05-27 23:19:03 +02:00
committed by GitHub
parent 0d2657ee37
commit 6d2dd37414
34 changed files with 1692 additions and 1296 deletions
+4 -2
View File
@@ -43,7 +43,7 @@ router.get('/login', async (req: Request, res: Response) => {
const redirectUri = `${appUrl.replace(/\/+$/, '')}/api/auth/oidc/callback`;
const inviteToken = req.query.invite as string | undefined;
const state = createState(redirectUri, inviteToken);
const { state, codeChallenge } = createState(redirectUri, inviteToken);
const params = new URLSearchParams({
response_type: 'code',
@@ -51,6 +51,8 @@ router.get('/login', async (req: Request, res: Response) => {
redirect_uri: redirectUri,
scope: process.env.OIDC_SCOPE || 'openid email profile',
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
res.redirect(`${doc.authorization_endpoint}?${params}`);
@@ -92,7 +94,7 @@ router.get('/callback', async (req: Request, res: Response) => {
try {
const doc = await discover(config.issuer, config.discoveryUrl);
const tokenData = await exchangeCodeForToken(doc, code, pending.redirectUri, config.clientId, config.clientSecret);
const tokenData = await exchangeCodeForToken(doc, code, pending.redirectUri, config.clientId, config.clientSecret, pending.codeVerifier);
if (!tokenData._ok || !tokenData.access_token) {
console.error('[OIDC] Token exchange failed: status', tokenData._status);
return res.redirect(frontendUrl('/login?oidc_error=token_failed'));