Files
f44d88e134 feat(password): email-confirmed password setup for SSO accounts (#10649)
* feat(password): add email-confirmed password setup for SSO accounts

SSO-only accounts (Google, GitHub, OIDC) now have a secure path to add
a password credential without requiring direct session trust.

**Problem:** Previously, password setup for SSO users either required an
existing password (blocking SSO-only users entirely) or would have needed
to trust the session token alone to create a persistent credential — a
security gap where a compromised session could silently add a password.

**Solution:** Email-confirmed flow that reuses the existing recovery
infrastructure:

1. `checkHasPassword` RPC — authenticates via session token, returns
   whether the account has a password hash set (drives UI branching).
2. `requestPasswordSetup` RPC — authenticates via session token, looks up
   the account's verified email social ID, generates a recovery token
   (`restoreEmail` claim), and sends a "Password recovery" email via the
   existing mail service. No DB schema changes.
3. `PasswordRestore.svelte` (unchanged) handles the link click → calls
   the existing `restorePassword` RPC → password is set.

**UI changes** (`Password.svelte`):
- `hasPassword === false` → "Set a password" panel with description and
  "Send setup link" button
- On success → "Check your email for a link to set your password."
- On `SocialIdNotFound` → "No email address is linked to your account."
  with guidance to add one via Account Settings → Manage Identities
- `hasPassword === true` → existing "Change password" form (unchanged)

**Account client:** Added `checkHasPassword()` and
`requestPasswordSetup()` methods to `AccountClientImpl`; both registered
as platform resource functions (`login.function.CheckHasPassword` /
`login.function.RequestPasswordSetup`).

Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Don Kendall <kendall@donkendall.com>

* test(password): add unit tests for SSO password setup RPCs

ssoPassword.test.ts — 12 tests covering:
- checkHasPassword: returns true/false for hash+salt presence, false for
  partial state (hash-only or salt-only), error for missing account
- changePassword: rejects empty old/new passwords, rejects wrong
  oldPassword (hash mismatch)
- requestPasswordSetup: sends email when email social ID exists, returns
  SocialIdNotFound when no email is linked, handles mail service failures
  gracefully (logs error, does not rethrow)

signupTokenGuard.test.ts — added edge-case for empty-string token to
document current guard behaviour (token != null passes empty string
through; noted as a future hardening opportunity).

Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Don Kendall <kendall@donkendall.com>

* chore(dev): add dev-local webpack proxy for local Docker compose stack

Adds a `dev-local` CLIENT_TYPE that proxies webpack dev server requests
to a local Docker compose stack (nginx at localhost:8088), following the
same pattern as the existing `dev-server`, `dev-huly`, etc. modes.

Useful for developing frontend changes against a fully running local
backend without needing `huly.local` DNS configuration.

Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Don Kendall <kendall@donkendall.com>

* feat(email): redesign transactional emails with proper HTML and dedicated password setup template

All account service email templates were bare <p> tags with no styling,
branding, or call-to-action buttons. Replaced with production-quality
HTML emails using email-safe table layout and inline CSS.

Design: Huly wordmark on dark (#18181B) header, white card body, dark
CTA button, subtle border, system font stack. Plain-text versions
updated to match for clients that prefer text.

Templates improved:
- RecoveryHTML/Text — password reset flow
- ConfirmationHTML/Text — email verification on signup
- InviteHTML/Text — workspace invitation
- ResendInviteHTML/Text — re-invitation
- OtpHTML/Text — sign-in code with large monospace code display

New dedicated template for SSO password setup (PasswordSetupHTML/Text/
Subject) so the setup email has copy distinct from forgot-password
recovery. requestPasswordSetup now uses these instead of RecoveryHTML.
Subject: "Set a password for your Huly account".

Other language files updated with the new PasswordSetup* keys
(English copy as fallback — translations can follow separately).

Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Don Kendall <kendall@donkendall.com>

* fix(password): guard requestPasswordSetup against accounts with existing password

Add server-side check that rejects requestPasswordSetup calls from accounts
that already have a password hash+salt. The setup flow bypasses the
old-password requirement in changePassword, so it must be restricted to
SSO-only accounts. The UI already guards this branch but defence-in-depth
requires the server to enforce it independently.

Also adds JSDoc to requestPasswordSetup and extends unit test coverage:
- TokenError path for checkHasPassword (invalid/expired token)
- BadRequest guard for requestPasswordSetup on password-bearing accounts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Don Kendall <kendall@donkendall.com>

* fix: add missing locale keys and fix eslint/formatting for CI

- Add 5 missing SSO password translation keys to all non-en locale files
  (SetPassword, SSOPasswordDescription, SendSetupLink, SSOPasswordEmailSent,
  SSONoEmailLinked) to fix locale parity test
- Replace non-null assertions with type casts in ssoPassword.test.ts
  to fix @typescript-eslint/no-non-null-assertion errors
- Revert unrelated tracker/github cosmetic changes that triggered
  pre-existing eslint errors in those packages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Don Kendall <kendall@donkendall.com>

* fix: address review — remove dev/prod changes, translate PasswordSetup strings

- Revert dev/prod/webpack.config.js and package.json (per BykhovDenis)
- Translate PasswordSetupText and PasswordSetupSubject for all 10 locales
  (cs, de, es, fr, it, pt-br, pt, ru, tr, zh)
- PasswordSetupHTML stays in English (reviewer approved)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Don Kendall <kendall@donkendall.com>

---------

Signed-off-by: Don Kendall <dkendall@ledoweb.com>
Signed-off-by: Don Kendall <kendall@donkendall.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 03:53:18 +05:00

23 lines
6.8 KiB
JSON

{
"string": {
"ConfirmationText": "Grazie per il tuo interesse in {name}. Per completare il processo di registrazione, ti preghiamo di incollare il seguente link nella barra degli indirizzi del tuo browser: {link}. Cordiali saluti, il team di {name}.",
"ConfirmationHTML": "<p>Ciao,</p><p>Grazie per il tuo interesse in {name}. Per completare il processo di registrazione, ti preghiamo di cliccare <a href={link}>su questo link</a> o di incollare il seguente link nella barra degli indirizzi del tuo browser.</p><p>{link}</p><p>Cordiali saluti,</p><p>il team di {name}.</p>",
"ConfirmationSubject": "Conferma il tuo indirizzo email per registrarti a {name}",
"RecoveryText": "Abbiamo ricevuto una richiesta per reimpostare la password del tuo account. Per reimpostare la tua password, ti preghiamo di incollare il seguente link nella barra degli indirizzi del tuo browser: {link}. Se non hai richiesto un ripristino della password, ignora questa email.",
"RecoveryHTML": "<p>Abbiamo ricevuto una richiesta per reimpostare la password del tuo account. Per reimpostare la tua password, ti preghiamo di cliccare sul link qui sotto: <a href={link}>Reimposta la password</a></p><p>Se il link per reimpostare la password sopra non funziona, incolla il seguente link nella barra degli indirizzi del tuo browser: {link}</p><p>Se non hai richiesto un ripristino della password, ignora questa email.</p>",
"RecoverySubject": "Recupero password",
"InviteText": "Sei stato invitato a {ws}. Per unirti, ti preghiamo di incollare il seguente link nella barra degli indirizzi del tuo browser: {link}. Il link è valido per {expHours} ore.",
"InviteHTML": "<p>Sei stato invitato a {ws}. Per unirti, ti preghiamo di cliccare sul link qui sotto: <a href={link}>Unisciti</a></p><p>Se il link di invito sopra non funziona, incolla il seguente link nella barra degli indirizzi del tuo browser: {link}</p><p>Il link è valido per {expHours} ore.</p>",
"InviteSubject": "Invito a {ws}",
"OtpText": "Conferma il tuo indirizzo email per accedere a {app}!\n\nIl tuo codice di conferma è qui sotto - inseriscilo nella finestra dove hai iniziato a effettuare il login per {app}.\n\n{code}\n\nSe non hai richiesto questa email, non c'è nulla di cui preoccuparsi: puoi ignorarla in tutta sicurezza.",
"OtpHTML": "<h3>Conferma il tuo indirizzo email per accedere a {app}!</h3><p>Il tuo codice di conferma è qui sotto - inseriscilo nella finestra dove hai iniziato a effettuare il login per {app}.</p><br/><b style=\"font-size:36px\">{code}</b><br/><br/><p>Se non hai richiesto questa email, non c'è nulla di cui preoccuparsi: puoi ignorarla in tutta sicurezza.</p>",
"OtpSubject": "Codice di conferma {app}: {code}",
"ResendInviteText": "Sei stato reinvitato a {ws}. Utilizza il seguente link per unirti: {link}. Il link di reinvito è valido per {expHours} ore.",
"ResendInviteHTML": "<p>Sei stato reinvitato a {ws}. Per unirti, fai clic sul link sottostante: <a href={link}>Unisciti</a></p><p>Se il link di invito sopra non funziona, incolla il seguente link nella barra degli URL del tuo browser: {link}</p><p>Il link di reinvito è valido per {expHours} ore.</p>",
"ResendInviteSubject": "Reinvito a {ws}",
"PasswordSetupText": "Imposta una password per il tuo account\n\nHai richiesto di aggiungere l'accesso con password al tuo account.\n\nImposta la tua password qui:\n{link}\n\nUna volta impostata, potrai accedere con e-mail e password in aggiunta al tuo metodo di accesso esistente.\n\nSe non hai richiesto questo, puoi ignorare questa email in tutta sicurezza.",
"PasswordSetupHTML": "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head><body style=\"margin:0;padding:0;background-color:#F2F2F7;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased\"><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background-color:#F2F2F7;padding:40px 16px\"><tr><td align=\"center\"><table role=\"presentation\" width=\"520\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"max-width:520px;width:100%\"><tr><td style=\"background-color:#18181B;border-radius:12px 12px 0 0;padding:24px 40px\"><span style=\"display:inline-block;font-size:18px;font-weight:700;color:#FFFFFF;letter-spacing:-0.3px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif\">Huly</span></td></tr><tr><td style=\"background-color:#FFFFFF;padding:36px 40px;border:1px solid #E4E4E7;border-top:none;border-radius:0 0 12px 12px\"><h1 style=\"margin:0 0 12px 0;font-size:22px;font-weight:700;color:#18181B;letter-spacing:-0.4px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;line-height:1.3\">Set a password for your account</h1><p style=\"margin:0 0 28px 0;font-size:15px;line-height:1.65;color:#52525B;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif\">You requested to add password sign-in to your account. Click the button below to choose your password.</p><p style=\"margin:0 0 16px 0;font-size:13px;line-height:1.6;color:#71717A;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif\">Once set, you can sign in with email + password in addition to your existing sign-in method.</p><table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td style=\"background-color:#18181B;border-radius:8px\"><a href=\"{link}\" target=\"_blank\" style=\"display:inline-block;padding:13px 26px;font-size:14px;font-weight:600;color:#FFFFFF;text-decoration:none;letter-spacing:-0.1px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif\">Set password &rarr;</a></td></tr></table><p style=\"margin:28px 0 0 0;font-size:12px;line-height:1.6;color:#A1A1AA;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif\">Button not working? Copy and paste this link into your browser:<br><a href=\"{link}\" style=\"color:#71717A;word-break:break-all;text-decoration:underline;font-size:12px\">{link}</a></p><table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"margin-top:28px\"><tr><td style=\"border-top:1px solid #F4F4F5;padding-top:24px\"><p style=\"margin:0;font-size:12px;color:#A1A1AA;line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif\">If you didn&apos;t request this, you can safely ignore this email.</p></td></tr></table></td></tr><tr><td align=\"center\" style=\"padding:20px 0\"><p style=\"margin:0;font-size:12px;color:#9CA3AF;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif\">&copy; Huly &mdash; All rights reserved</p></td></tr></table></td></tr></table></body></html>",
"PasswordSetupSubject": "Imposta una password per il tuo account Huly"
}
}