* docs(email): clarify 888888 is opt-in via MULTICA_DEV_VERIFICATION_CODE; document SMTP option in self-host docs
The startup log line, .env.example, and SELF_HOSTING_ADVANCED.md still
implied that the dev master code 888888 is auto-active whenever
APP_ENV != "production". That has not been true since the master code
was gated behind MULTICA_DEV_VERIFICATION_CODE — the fixed code is
disabled by default and must be opted in explicitly.
Also extend the docs site with the SMTP relay backend added in #1877:
auth-setup, environment-variables, and self-host-quickstart now cover
both Resend and SMTP options in EN and ZH.
Co-authored-by: multica-agent <github@multica.ai>
* docs(email): treat SMTP as an email backend in self-host docs and startup warning
Address review feedback on #2666:
- server: startup warning now fires only when both RESEND_API_KEY and SMTP_HOST
are empty, since either one is a valid email backend. Otherwise the log
mis-tells SMTP-only operators that verification codes go to stdout.
- self-host-quickstart (EN/ZH): tell readers to fetch the verification code
from whichever backend they configured (Resend or SMTP); fall back to
stdout only when neither is configured.
- auth-setup (EN/ZH): \"without Resend\" → \"without any email backend
configured\" so the wording stays correct now that SMTP is a first-class
option.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(email): add SMTP relay as alternative to Resend
Self-hosted deployments often run behind a corporate firewall with an
existing SMTP relay (Exchange, Postfix, sendmail) and no access to
external SaaS APIs. Resend requires a public domain, an API key, and
outbound HTTPS to api.resend.com — all unavailable in air-gapped or
private-network setups.
This adds a second email delivery path using Go's stdlib net/smtp,
activated when SMTP_HOST is set. Priority order:
1. SMTP relay (SMTP_HOST set)
2. Resend API (RESEND_API_KEY set)
3. DEV stdout (neither set)
New env vars (all optional, no breaking change):
SMTP_HOST — SMTP server hostname
SMTP_PORT — port, default 25
SMTP_USERNAME — for authenticated SMTP; empty = unauthenticated relay
SMTP_PASSWORD — used only when SMTP_USERNAME is set
SMTP_TLS_INSECURE — set to "true" to skip TLS cert verification
(for private CA / self-signed certs)
The implementation:
- Dials TCP, creates smtp.Client manually (avoids smtp.SendMail which
does not expose TLS config)
- Tries STARTTLS if advertised; uses InsecureSkipVerify only when
SMTP_TLS_INSECURE=true (opt-in, nolint:gosec annotated)
- Applies PlainAuth only when SMTP_USERNAME is non-empty
- Wraps all errors with context for easier debugging
- Reuses existing HTML templates from buildInvitationParams for
invitation emails (no template duplication)
Also updates .env.example and docker-compose.selfhost.yml with the
new variables and inline documentation.
* fix(email): add dial timeout, session deadline, RFC headers for SMTP path
Address review blockers from multica-eve and Bohan-J (PR #1877):
- net.Dial → net.DialTimeout(10s) + conn.SetDeadline(30s) so a blackholed
SMTP relay cannot hang SendVerificationCode (called synchronously from the
auth handler) or leak goroutines in the invitation path.
- Add Date, Message-ID, and proper Content-Transfer-Encoding headers.
Date is required by RFC 5322; many strict relays reject messages without it.
Message-ID aids deliverability and threading.
- MIME-encode Subject via mime.QEncoding so non-ASCII workspace/inviter names
(CJK, emoji) survive without corruption across any RFC 2047-conformant relay.
- Probe 8BITMIME after (possible) STARTTLS: use Content-Transfer-Encoding 8bit
when the relay advertises 8BITMIME, quoted-printable otherwise — safe for
all relay configurations without forcing base64 overhead.
- Update SELF_HOSTING_ADVANCED.md to document Option B (SMTP relay) alongside
the existing Resend section, including all five env vars and a note that
port 465/SMTPS is not yet supported.
* fix(email): correct has8Bit assignment order (bool is first return of Extension)
Follow-up to #1126 (which closed the HTML-injection vector in the Body).
The Subject line is not HTML-rendered, so html.EscapeString would leak
literal entities into recipient inboxes. Instead:
- Strip control characters from workspace/inviter names (defense in depth
even though Resend also filters CR/LF).
- Cap each field at 60 runes so an attacker can't stuff a full phishing
pitch into a workspace name that gets sent from noreply@multica.ai.
Also extracts buildInvitationParams to make the sanitization logic
testable without mocking the Resend SDK, and adds a test covering:
- HTML escape behavior for script/attribute/anchor injection payloads
- Subject stripping of \r\n\t and other unicode controls
- Subject NOT being HTML-escaped (so "Acme & Co." stays literal)
- Subject length bounds
- Benign inputs pass through unchanged
Adds a note on SendVerificationCode that its body uses only
server-generated content, to prevent the same pitfall from creeping in.
Refs #1117
* fix(email): HTML-escape workspace/inviter names in invitation email
SendInvitationEmail interpolated workspaceName and inviterName directly
into the HTML body via fmt.Sprintf with no escaping. A workspace owner
who sets a name like '</h2><a href="https://evil.example">Click</a>'
can break the email structure and inject attacker-controlled links that
appear as part of the official Multica invitation.
Escape both values with html.EscapeString before interpolation. The
Subject line also gets the escaped variants since some transports render
HTML-entity-like sequences.
Closes#1117
* fix(email): use raw names in Subject, keep HTML-escape for body only
Email Subject is a plain-text context — applying html.EscapeString
turns "A&B" into "A&B" and "O'Brien" into "O'Brien" in the
recipient's inbox. Keep the escape for the Html body where it prevents
injection, but use the original values in Subject.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: shaun0927 <shaun0927@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The email CTA now deep-links to /invite/{id} instead of the generic app
URL. If the user isn't logged in, they're redirected to login with a
?next= param that brings them back to the invite page.
Changes:
- Backend: GET /api/invitations/{id} endpoint (enriched with workspace/inviter names)
- Backend: Email template now links to /invite/{invitationId}
- Frontend: Shared InvitePage component (packages/views/invite/)
- Frontend: Web route at (auth)/invite/[id], Desktop route at invite/:id
- Frontend: /invite/ excluded from navigation history persistence
Uses the existing Resend email service to notify invitees.
Email includes inviter name, workspace name, and a link to the app.
Sent fire-and-forget in a goroutine to avoid blocking the API response.
* feat(auth): add email verification login flow with 401 auto-redirect
Replace the old OAuth-based login with email verification codes:
- Backend: send-code / verify-code endpoints, verification_codes table (migration 009), rate limiting, Resend email service
- Frontend: two-step login UI (email → 6-digit OTP), auth store with sendCode/verifyCode
- SDK: ApiClient gains onUnauthorized callback; 401 responses auto-clear token and redirect to /login
- Fix login button staying disabled due to global isLoading state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): add brute-force protection, redirect loop guard, and expired code cleanup
- VerifyCode: increment attempts on wrong code, reject after 5 failed tries (migration 010)
- onUnauthorized: skip redirect if already on /login to prevent infinite loops
- SendCode: best-effort cleanup of expired verification codes older than 1 hour
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(auth): add master verification code for non-production environments
Allow code "888888" to bypass email verification in non-production
environments to simplify development and testing workflows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(auth): add personal access tokens for CLI and API authentication
Add full-stack PAT support: users create tokens in Settings, CLI authenticates
via `multica auth login`. Server stores SHA-256 hashes only. Auth middleware
extended to accept both JWTs and PATs (distinguished by `mul_` prefix).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>