mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
* fix(email): wire SMTP_EHLO_NAME through self-host config + docs Follow-up to #3679, which added SMTP_EHLO_NAME in code but never exposed it to operators. - docker-compose.selfhost.yml: pass SMTP_EHLO_NAME through to the backend container. The compose env block is an explicit allowlist, so without this the override set in .env was silently dropped and never reached the process — making the escape hatch unusable on the docker path. - Document the var alongside its SMTP_* siblings: .env.example, SELF_HOSTING_ADVANCED.md, environment-variables.mdx, auth-setup.mdx, and self-host-quickstart.mdx (the last two with a strict-relay example). - email.go: log when os.Hostname() fails instead of silently falling back to net/smtp's lazy "localhost" — the exact greeting strict relays reject. - Add TestNewEmailService_EHLOName covering the env override, trimming, and the hostname fallback. MUL-2984 Co-authored-by: multica-agent <github@multica.ai> * fix(email): gate EHLO resolution to SMTP mode + sync docs to zh/ja/ko Addresses review nits on this PR: - email.go: resolve smtpEHLOName only when SMTP_HOST is set, so the Resend / DEV-stdout paths never call os.Hostname() or emit its failure log. The EHLO name is only ever used on the SMTP send path. - docs: add SMTP_EHLO_NAME to the zh/ja/ko variants of environment-variables, self-host-quickstart, and auth-setup, in sync with the English docs updated earlier in this PR. Note: the ja/ko self-host-quickstart and auth-setup pages were already missing the port-465 implicit-TLS example (pre-existing i18n drift from an earlier SMTP_TLS change, unrelated to this PR); the new EHLO block is inserted at the correct logical anchor regardless. A full ja/ko re-sync is left as a separate follow-up. MUL-2984 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
187 lines
9.6 KiB
Plaintext
187 lines
9.6 KiB
Plaintext
---
|
||
title: 登录与注册配置
|
||
description: 配 Email 验证码登录、Google OAuth、注册白名单和本地测试验证码。
|
||
---
|
||
|
||
import { Callout } from "fumadocs-ui/components/callout";
|
||
import { Mermaid } from "@/components/mermaid";
|
||
|
||
Multica 支持两种登录方式:**Email + 验证码**(默认)和 **Google OAuth**(可选)。登录成功后 server 签发一个 30 天有效期的 JWT cookie。这一页讲怎么配、怎么限制谁能注册、以及本地测试验证码怎么安全使用。
|
||
|
||
上面用到的环境变量的清单见 [环境变量](/environment-variables);token 怎么用、生命周期细节见 [认证与令牌](/auth-tokens)。
|
||
|
||
## Email + 验证码登录怎么工作
|
||
|
||
用户在登录页输邮箱 → server 发 6 位验证码 → 用户填回 → server 验证 → 签发 JWT cookie。是标准流程。支持两种邮件发送通道,按部署环境二选一:
|
||
|
||
### Option A:Resend(公网/云端部署推荐)
|
||
|
||
1. 在 [Resend](https://resend.com/) 建账号、验证你的域名
|
||
2. 创建 API key
|
||
3. 设环境变量:
|
||
|
||
```bash
|
||
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
|
||
RESEND_FROM_EMAIL=noreply@yourdomain.com # 必须是 Resend 已验证的域名
|
||
```
|
||
|
||
4. 重启 server
|
||
|
||
### Option B:SMTP relay(内网/自部署)
|
||
|
||
适合内网无法访问 `api.resend.com`,或者已经有内部邮件中继(Microsoft Exchange、Postfix、自部署 SendGrid 等)的场景。同时设置时 `SMTP_HOST` 优先级高于 `RESEND_API_KEY`:只要 `SMTP_HOST` 非空,server 一律走 SMTP 路径,即便 `RESEND_API_KEY` 也配着,验证码和邀请邮件也不会经过 Resend 外发出内网。
|
||
|
||
SMTP 路径覆盖大多数本地邮件服务器(特别是 Microsoft Exchange 的 receive connector)暴露的三种 relay 模式:
|
||
|
||
| 模式 | 端口 | 认证 | TLS |
|
||
|---|---|---|---|
|
||
| 匿名内部 relay | `25` | 无 —— 按 IP / 子网信任 | 链路上无 TLS(仅限内网段) |
|
||
| 认证提交(submission) | `587` | `SMTP_USERNAME` + `SMTP_PASSWORD` | STARTTLS,自动升级 |
|
||
| 隐式 TLS(SMTPS) | `465` | 可选(`SMTP_USERNAME` + `SMTP_PASSWORD`) | 连上即 TLS 握手 —— 端口 `465` 自动启用;非标准端口设 `SMTP_TLS=implicit` 强制开启 |
|
||
|
||
**匿名 Exchange relay,端口 25** —— 经典的 "internal SMTP relay" / Exchange 匿名 receive connector,按可信子网放行,不要求凭据:
|
||
|
||
```bash
|
||
SMTP_HOST=exchange.internal.example.com
|
||
SMTP_PORT=25
|
||
SMTP_USERNAME=
|
||
SMTP_PASSWORD=
|
||
SMTP_TLS_INSECURE=false
|
||
RESEND_FROM_EMAIL=noreply@yourdomain.com # 同时作为 SMTP From: 头
|
||
```
|
||
|
||
**认证提交,端口 587** —— 需要 service account 的 relay;服务端 advertise STARTTLS 时会自动升级:
|
||
|
||
```bash
|
||
SMTP_HOST=smtp.internal.example.com
|
||
SMTP_PORT=587
|
||
SMTP_USERNAME=multica
|
||
SMTP_PASSWORD=...
|
||
SMTP_TLS_INSECURE=false # 仅在私有 CA / 自签证书时改成 true
|
||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||
```
|
||
|
||
**隐式 TLS(SMTPS),端口 465** —— 适用于只提供 SMTPS、不广告 STARTTLS 的服务商(例如阿里云 / 腾讯企业邮箱)。端口 `465` 会自动启用隐式 TLS;非标准 SMTPS 端口设置 `SMTP_TLS=implicit`(别名:`smtps`、`ssl`)即可强制开启:
|
||
|
||
```bash
|
||
SMTP_HOST=smtp.qiye.aliyun.com
|
||
SMTP_PORT=465 # 465 自动启用隐式 TLS
|
||
SMTP_USERNAME=multica@yourdomain.com
|
||
SMTP_PASSWORD=...
|
||
SMTP_TLS=implicit # 465 上可省略;在非标准 SMTPS 端口上必填
|
||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||
```
|
||
|
||
**严格公网 relay(例如 Google Workspace `smtp-relay.gmail.com`)**还要求一个合法的 EHLO 名称。它们会拒绝来自公网 IP 的默认 `localhost` 问候,relay 随即断开连接——这不会在问候阶段报错,而是在后续某条命令上表现为一个不知所云的 `EOF`(`smtp auth: EOF`)。把 `SMTP_EHLO_NAME` 设成 relay 期望的 FQDN;它默认取机器主机名,而在容器内这通常不是合法的 FQDN:
|
||
|
||
```bash
|
||
SMTP_HOST=smtp-relay.gmail.com
|
||
SMTP_PORT=587
|
||
SMTP_EHLO_NAME=mail.yourdomain.com # relay 接受的 FQDN;默认取(非 FQDN 的)容器主机名
|
||
RESEND_FROM_EMAIL=noreply@yourdomain.com
|
||
```
|
||
|
||
启动时 server 会打印当前选择的 provider 和协商出的 TLS 模式,比如 `EmailService: SMTP relay exchange.internal.example.com:25 (starttls) from=noreply@example.com` 或 `… smtp.qiye.aliyun.com:465 (implicit-tls) from=…`(或 `Resend API` / `DEV mode`),密码不会出现在日志里。重启后没看到 SMTP 这行,说明 `SMTP_HOST` 没进到进程,确认下容器环境(`docker compose -f docker-compose.selfhost.yml exec backend env | grep SMTP`)。
|
||
|
||
**两种都不配**:server 不报错,但所有本该发出去的邮件**只打到 server 的 stdout**。本地开发方便(你从日志抄验证码),生产环境等于黑洞。
|
||
|
||
## 固定本地测试验证码
|
||
|
||
<Callout type="warning">
|
||
**不要在公网可访问实例上启用固定验证码。**
|
||
|
||
旧版「非 production 默认接受 `888888`」的行为已经移除。除非你显式配置,否则输入 `888888` 会和普通错误验证码一样被拒绝。
|
||
|
||
没配任何邮件后端(Resend 和 SMTP 都没设)的本地开发,应使用 server 日志里打印的随机验证码。如果你需要确定性的本地/私有自动化测试,可以把 `MULTICA_DEV_VERIFICATION_CODE` 设成一个 6 位数字,比如 `888888`,并保持 `APP_ENV` 为非 production:
|
||
|
||
```bash
|
||
APP_ENV=development
|
||
MULTICA_DEV_VERIFICATION_CODE=888888
|
||
```
|
||
|
||
`APP_ENV=production` 时这个快捷码会被忽略。
|
||
</Callout>
|
||
|
||
生产部署应保持 `MULTICA_DEV_VERIFICATION_CODE` 为空,并设置 `APP_ENV=production`。如果你用 `make selfhost` / `docker-compose.selfhost.yml` 自部署,`APP_ENV` 默认就是 `production`。
|
||
|
||
## 怎么配 Google OAuth
|
||
|
||
可选。不配就只有 Email + 验证码登录;配了后登录页会多出「用 Google 登录」按钮。
|
||
|
||
1. 去 [Google Cloud Console](https://console.cloud.google.com/) 创建一个 OAuth 2.0 client
|
||
2. **授权的回调 URI**(Authorized redirect URIs)填你的 Multica 前端地址加 `/auth/callback`,例如:
|
||
|
||
```text
|
||
https://multica.yourdomain.com/auth/callback
|
||
```
|
||
|
||
3. 拿到 client ID 和 client secret 后设三个环境变量:
|
||
|
||
```bash
|
||
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
|
||
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxx
|
||
GOOGLE_REDIRECT_URI=https://multica.yourdomain.com/auth/callback
|
||
```
|
||
|
||
4. 重启 server。
|
||
|
||
**热生效**:前端通过 `/api/config` 运行时读这些配置——改完只要重启 server,前端不用重建镜像、不用重新部署。
|
||
|
||
<Callout type="warning">
|
||
**回调 URI 在 Google Console 和 `GOOGLE_REDIRECT_URI` 两处必须完全一致**,包括协议(`http` vs `https`)、尾部斜杠、端口。不一致 Google 会拒绝整个 OAuth 流程,用户看到的错误是 `redirect_uri_mismatch`。
|
||
</Callout>
|
||
|
||
## 怎么限制谁能注册
|
||
|
||
三层环境变量按优先级组合:
|
||
|
||
<Mermaid chart={`
|
||
graph TD
|
||
Start[新用户首次登录] --> A{email 在<br/>ALLOWED_EMAILS 里?}
|
||
A -- 是 --> Allow[允许注册]
|
||
A -- 否 --> B{domain 在<br/>ALLOWED_EMAIL_DOMAINS 里?}
|
||
B -- 是 --> Allow
|
||
B -- 否 --> C{任一白名单<br/>非空?}
|
||
C -- 是 --> Block[拒绝]
|
||
C -- 否 --> D{ALLOW_SIGNUP<br/>= true?}
|
||
D -- 是 --> Allow
|
||
D -- 否 --> Block
|
||
`} />
|
||
|
||
**已经登录过的老用户永远可以再次登录**——signup 白名单只对**首次注册**生效,不拦截老用户。
|
||
|
||
- **`ALLOWED_EMAILS`**(最高优先级)—— 显式邮箱白名单,逗号分隔。**非空时只有列表里的邮箱能注册**。
|
||
- **`ALLOWED_EMAIL_DOMAINS`**—— 域名白名单,逗号分隔(例如 `company.io,partner.com`)。
|
||
- **`ALLOW_SIGNUP`** —— 总开关,默认 `true`。设 `false` 完全关闭注册。
|
||
|
||
<Callout type="warning">
|
||
**三层白名单是 AND 语义,不是 OR。** 很多人第一直觉是「设 `ALLOWED_EMAIL_DOMAINS=company.io` + `ALLOW_SIGNUP=true` 就是允许 company.io 和其他所有人」——**不是**。任何一层白名单只要设了非空值,**不匹配的邮箱直接拒**,`ALLOW_SIGNUP=true` 挡不住。
|
||
|
||
要真的「允许所有人」,所有三个环境变量都留空(或 `ALLOW_SIGNUP=true`)。
|
||
</Callout>
|
||
|
||
**典型配法**:
|
||
|
||
| 需求 | 配置 |
|
||
|---|---|
|
||
| 公司内网,只允许 `company.io` 员工 | `ALLOWED_EMAIL_DOMAINS=company.io` |
|
||
| 公司内网 + 几个外部合作者 | `ALLOWED_EMAIL_DOMAINS=company.io` + 合作者个人邮箱加到 `ALLOWED_EMAILS` |
|
||
| 完全关闭自助注册,只能邀请 | `ALLOW_SIGNUP=false` |
|
||
| 开放注册(不推荐生产用)| 三个都留空 |
|
||
|
||
## 关了注册还能邀请人进来吗
|
||
|
||
**只对已经有 Multica 账号的人能**。接受邀请那一步不检查 signup 白名单——如果对方已经注册过(比如在别的工作区),他们点链接登录就能直接接受。
|
||
|
||
**但还没注册过的人,邀请救不了他们**。他们接受邀请前必须先登录,登录的第一步(发验证码)会过 signup 白名单检查。如果你 `ALLOW_SIGNUP=false`、或他们的邮箱不在 `ALLOWED_EMAILS` / `ALLOWED_EMAIL_DOMAINS` 里,他们**没法完成注册**,也就没法接受邀请。
|
||
|
||
要邀请一个还没注册的外部协作者:临时把他们的邮箱加到 `ALLOWED_EMAILS`,等他们注册 + 接受邀请之后再把这条移掉。
|
||
|
||
邀请的创建和使用见 [成员与权限](/members-roles)。
|
||
|
||
## 下一步
|
||
|
||
- [环境变量](/environment-variables) —— 这一页用到的环境变量完整定义
|
||
- [认证与令牌](/auth-tokens) —— JWT / PAT / Daemon Token 的分类和使用
|
||
- [故障排查](/troubleshooting) —— 验证码收不到、OAuth 报 `redirect_uri_mismatch`、注册被拒的常见排查
|