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": "Děkujeme za váš zájem o {name}. Pro dokončení procesu registrace prosím vložte následující odkaz do adresního řádku vašeho prohlížeče: {link}. S pozdravem, tým {name}.",
"ConfirmationHTML": "<p>Dobrý den,</p><p>Děkujeme za váš zájem o {name}. Pro dokončení procesu registrace klikněte prosím na <a href={link}>tento odkaz</a> nebo vložte následující odkaz do adresního řádku vašeho prohlížeče.</p><p>{link}</p><p>S pozdravem,</p><p>tým {name}.</p>",
"ConfirmationSubject": "Potvrďte svou e-mailovou adresu pro registraci do {name}",
"RecoveryText": "Obdrželi jsme žádost o obnovení hesla k vašemu účtu. Pro obnovení hesla prosím vložte následující odkaz do adresního řádku vašeho prohlížeče: {link}. Pokud jste žádost o obnovení hesla nepodali, ignorujte tento e-mail.",
"RecoveryHTML": "<p>Obdrželi jsme žádost o obnovení hesla k vašemu účtu. Pro obnovení hesla klikněte na níže uvedený odkaz: <a href={link}>Obnovit heslo</a></p><p>Pokud výše uvedený odkaz nefunguje, vložte následující odkaz do adresního řádku vašeho prohlížeče: {link}</p><p>Pokud jste žádost o obnovení hesla nepodali, ignorujte tento e-mail.</p>",
"RecoverySubject": "Obnovení hesla",
"InviteText": "Byli jste pozváni do {ws}. Pro připojení prosím vložte následující odkaz do adresního řádku vašeho prohlížeče: {link}. Odkaz je platný po dobu {expHours} hodin.",
"InviteHTML": "<p>Byli jste pozváni do {ws}. Pro připojení klikněte na níže uvedený odkaz: <a href={link}>Připojit se</a></p><p>Pokud výše uvedený odkaz nefunguje, vložte následující odkaz do adresního řádku vašeho prohlížeče: {link}</p><p>Odkaz je platný po dobu {expHours} hodin.</p>",
"InviteSubject": "Pozvánka do {ws}",
"OtpText": "Potvrďte svou e-mailovou adresu pro přístup do {app}!\n\nVáš potvrzovací kód je uveden níže - zadejte jej do okna, ve kterém jste zahájili přihlášení do {app}.\n\n{code}\n\nPokud jste tento e-mail nevyžádali, nemusíte si dělat starosti — můžete jej bezpečně ignorovat.",
"OtpHTML": "<h3>Potvrďte svou e-mailovou adresu pro přístup do {app}!</h3><p>Váš potvrzovací kód je uveden níže - zadejte jej do okna, ve kterém jste zahájili přihlášení do {app}.<p/><br/><b style=\"font-size:36px\">{code}</b><br/><br/><p>Pokud jste tento e-mail nevyžádali, nemusíte si dělat starosti — můžete jej bezpečně ignorovat.</p>",
"OtpSubject": "{app}: potvrzovací kód {code}",
"ResendInviteText": "Byli jste znovu pozváni do {ws}. Použijte prosím následující odkaz pro připojení: {link}. Odkaz pro opětovné pozvání je platný po dobu {expHours} hodin.",
"ResendInviteHTML": "<p>Byli jste znovu pozváni do {ws}. Pro připojení klikněte na následující odkaz: <a href={link}>Připojit se</a></p><p>Pokud výše uvedený odkaz nefunguje, vložte následující odkaz do adresního řádku prohlížeče: {link}</p><p>Odkaz pro opětovné pozvání je platný po dobu {expHours} hodin.</p>",
"ResendInviteSubject": "Opětovné pozvání do {ws}",
"PasswordSetupText": "Nastavení hesla pro váš účet\n\nPožádali jste o přidání přihlašování heslem ke svému účtu.\n\nNastavte si heslo zde:\n{link}\n\nPo nastavení se budete moci přihlašovat pomocí e-mailu a hesla kromě stávajícího způsobu přihlášení.\n\nPokud jste o toto nepožádali, můžete tento e-mail bezpečně ignorovat.",
"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": "Nastavte si heslo pro svůj účet Huly"
}
}