* 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>
When a user's name has no spaces (common for CJK names like "西门吹雪"),
name.split(' ') returns a single-element array so the last name
destructures as undefined, causing a PostgreSQL NOT NULL constraint
violation on the last_name column.
- openid: prefer standard OIDC given_name/family_name claims when
available, fall back to splitting name with safe slice(1).join()
- github: same split fix using displayName ?? username
- loginOrSignUpWithProvider: add ?? '' defensive fallback at insertOne
to guard against any undefined last name reaching the DB
Fixes#10628
Signed-off-by: SaiVaraprasad Medapati <varaprasadreddy9676@gmail.com>
Co-authored-by: SaiVaraprasad Medapati <varaprasadreddy9676@gmail.com>
When MAIL_URL is configured the account service intentionally returns
token: undefined to enforce email confirmation before granting access.
SignupForm.svelte was calling logIn() unconditionally, which triggered
PUT /cookie with no Authorization header. The cookie endpoint returned
a 401 whose response body was not parseable as JSON, crashing the client
with "Unexpected token 'N', 'Not Found' is not valid JSON". The account
was created successfully but the user was stuck on the signup page.
- Guard logIn() with `result.token != null`, matching the pattern
already used in doLoginNavigate() in utils.ts
- Fix PUT /cookie 401 response to use ctx.res.writeHead + ctx.res.end
with the JSON body inline, consistent with the rest of the file.
Previously ctx.body was set (Koa pattern) then ctx.res.end() was
called with no body (raw Node pattern), so the body was never sent.
- Add unit tests for the token guard logic
Fixes#10518
Signed-off-by: Don Kendall <kendall@donkendall.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(account): handle empty array in $in SQL clause to prevent PostgreSQL syntax error
buildWhereClause now emits FALSE for empty $in arrays instead of invalid
IN () syntax. Also adds an early return in getWorkspacesInfoWithStatusByIds
so callers like the GitHub/Gmail/Backup services never hit the query with
an empty uuid list.
Fixes#10553
Signed-off-by: Yulian Diaz <5605867+spatialy@users.noreply.github.com>
* fix(account): also guard against non-array uuids in getWorkspacesInfoWithStatusByIds
Per review feedback from @ArtyomSavchenko: use Array.isArray() in addition to
the length check to handle runtime cases where uuids may not be an array.
Signed-off-by: Yulian Diaz <5605867+spatialy@users.noreply.github.com>
---------
Signed-off-by: Yulian Diaz <5605867+spatialy@users.noreply.github.com>
When signing up with a password on deployments with MAIL_URL set, the
account service returns token: undefined to enforce email confirmation.
SignupForm.svelte was calling logIn() unconditionally, which triggered
PUT /cookie without an Authorization header, returning a 401 with an
unparseable body and leaving the user stuck on the signup page.
- Guard logIn() in SignupForm.svelte with `result.token != null`, matching
the pattern already used in doLoginNavigate() in utils.ts
- Fix PUT /cookie 401 response to use Koa's ctx.status/ctx.body instead of
raw ctx.res.writeHead/end, so the error body is correctly serialized
- Add unit tests for the token guard logic
Fixes#10518
Signed-off-by: Yulian Diaz <5605867+spatialy@users.noreply.github.com>
* Fix every time compacting and put images back to backup
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
* Fix formatting
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
---------
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>