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.2 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"string": {
"ConfirmationText": "感谢您对 {name} 的兴趣。要完成注册过程,请将以下链接复制到您的浏览器的 URL 栏中 {link}。此致,{name} 团队。",
"ConfirmationHTML": "<p>您好,</p><p>感谢您对 {name} 的兴趣。要完成注册过程,请点击 <a href={link}>此链接</a> 或将以下链接复制到您的浏览器的 URL 栏中。</p><p>{link}</p><p>此致,</p><p>{name} 团队。</p>",
"ConfirmationSubject": "确认您的电子邮件地址以注册 {name}",
"RecoveryText": "我们收到了重置您账户密码的请求。要重置您的密码,请将以下链接粘贴到您的浏览器地址栏中:{link}。如果您没有请求密码重置,请忽略此邮件。",
"RecoveryHTML": "<p>我们收到了重置您账户密码的请求。要重置您的密码,请点击以下链接:<a href={link}>重置密码</a></p><p>如果上面的重置密码链接不起作用,请将以下链接粘贴到您的浏览器地址栏中:{link}</p><p>如果您没有请求密码重置,请忽略此邮件。</p>",
"RecoverySubject": "密码恢复",
"InviteText": "您被邀请加入 {ws}。要加入,请将以下链接粘贴到您的浏览器地址栏中:{link}。链接有效期为 {expHours} 小时。",
"InviteHTML": "<p>您被邀请加入 {ws}。要加入,请点击以下链接:<a href={link}>加入</a></p><p>如果上面的邀请链接不起作用,请将以下链接粘贴到您的浏览器地址栏中:{link}</p><p>链接有效期为 {expHours} 小时。</p>",
"InviteSubject": "邀请加入 {ws}",
"OtpText": "确认您的电子邮件地址以访问 {app}!\n\n您的确认码如下 - 请在您开始登录 {app} 的窗口中输入。\n\n{code}\n\n如果您没有请求此邮件,不用担心 — 您可以放心地忽略它。",
"OtpHTML": "<h3>确认您的电子邮件地址以访问 {app}</h3><p>您的确认码如下 - 请在您开始登录 {app} 的窗口中输入。<p/><br/><b style=\"font-size:36px\">{code}</b><br/><br/><p>如果您没有请求此邮件,不用担心 — 您可以放心地忽略它。</p>",
"OtpSubject": "{app} 确认码:{code}",
"ResendInviteText": "您已被重新邀请加入 {ws}。请使用以下链接加入:{link}。重新邀请链接的有效期为 {expHours} 小时。",
"ResendInviteHTML": "<p>您已被重新邀请加入 {ws}。要加入,请点击以下链接:<a href={link}>加入</a></p><p>如果上方的邀请链接无效,请将以下链接粘贴到您的浏览器地址栏中:{link}</p><p>重新邀请链接的有效期为 {expHours} 小时。</p>",
"ResendInviteSubject": "重新邀请加入 {ws}",
"PasswordSetupText": "为您的账户设置密码\n\n您请求为账户添加密码登录方式。\n\n在此设置密码:\n{link}\n\n设置完成后,您可以使用电子邮件和密码登录,同时保留现有的登录方式。\n\n如果您没有发起此请求,可以放心地忽略这封邮件。",
"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": "为您的 Huly 账户设置密码"
}
}