# apps/backend

Harbor Desk Phase C（go-zero）。

## 原則

| 做 | 不做 |
|----|------|
| `generate/api` + `make gen-api` | 手寫 handler／routes |
| `generate/database/mongo` + `make migrate-up` | 在 gateway 啟動時隱式建 index／seed |
| `monc` + `CacheRedis` | 自己半套 mongo cache |
| `internal/logic` + `module/*/domain|repository|usecase` | 業務塞進 handler |

## 指令

```bash
cd apps/backend
make build       # gateway + worker
make start       # build 後背景啟動 gateway + worker（PID/log 在 /tmp/haixun）
make stop
make restart
make test
make gen-api     # 改 .api 後
make migrate-up  # 改 generate/database 後
make init        # 只建立可重複執行的 operational indexes
make seeder      # 讀 etc/gateway.yaml 的 Seed.AdminPassword
make crawler-install # 安裝 dev_mode Chrome crawler 的 Playwright/Chromium
SCOUT_CRAWLER_TOKEN='...' make crawler-start
SCOUT_CRAWLER_TOKEN='...' make crawler-restart
make tidy
```

### Scout Chrome Crawler

`dev_mode=true` 時，Scout 只走私有 Playwright crawler，沒有同步有效 Chrome session 就會要求使用者先同步。

1. 在私有 `etc/gateway.yaml` 填 `Scout.SessionSecret`、`Scout.CrawlerEndpoint`、`Scout.CrawlerToken`。
2. 執行 `make crawler-install` 一次。
3. 執行 `SCOUT_CRAWLER_TOKEN='與 Scout.CrawlerToken 相同的值' make crawler-start`。
4. crawler 必須維持 `127.0.0.1` 私網綁定，禁止公開反向代理。

詳見 [`crawler/README.md`](crawler/README.md)。

```bash
# 需要 Mongo + Redis（go-zero monc / redis）
# 私有設定只放在 etc/gateway.yaml（此檔不會被 git 追蹤）：
# cp etc/gateway.example.yaml etc/gateway.yaml
# 填入 Mongo/Redis/MinIO 密碼與兩組不同的 JWT secret。
# Mongo 若有帳密（本機 infra 常見），一定要帶進 URI：
MONGO_URL='mongodb://USER:PASS@127.0.0.1:27017/haixun?authSource=admin' make migrate-up
go run . -f etc/gateway.yaml
# Seed.AdminPassword 填好後執行 make seeder（uid=1000000 → 顯示 01000000）
```

### 日誌與密碼（安全）

| 禁止出現在 log | 說明 |
|----------------|------|
| `password` / `new_password` / `current_password` / `temporary_password` | 明碼 |
| `password_hash` | bcrypt 也不可打 log |
| access/refresh token、api_key | 憑證 |

實作：

- 啟動時 `securelog.SilenceMongo()` → 關閉 go-zero **mon** 文件 dump（否則 ReplaceOne 會印出整份 member）
- `RestConf.Verbose = false` → 不印 request body
- 自寫 log 請用 `securelog.RedactAny(v)` / 勿 `logx.Infof("%+v", req)`

HTTP access log 只記 method/path/status/duration，不含 body。

### 物件儲存（頭像）— **僅 MinIO/S3**

不落本機磁碟（無 localstore / `/files`）。未設定 Endpoint 時 upload 直接失敗。

| 項目 | 說明 |
|------|------|
| API | `POST /api/v1/media/upload`（需 JWT）`{ content: dataURL, kind: "avatar" }` |
| 回傳 | `{ url, key }` — Mongo member **只存 url** |
| 後端 | MinIO（dev）或 S3（prod），S3 API + path-style |
| env | `HAIXUN_STORAGE_S3_*` 或 `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` |

```yaml
ObjectStorage:
  Endpoint: "http://127.0.0.1:9000"   # 必填
  Bucket: "haixun-assets"
  AccessKey: "haixun"
  SecretKey: "..."
  UsePathStyle: true
  PublicBaseURL: ""                   # 或 CDN / 反向代理公開前綴
```

FE：選頭像 → upload → URL → `PATCH profile`（禁止 data URL）。

### 密碼政策（前後端一致）

| 規則 | 說明 |
|------|------|
| 長度 | **≥ 12** 字元 |
| 組成 | **大寫 + 小寫 + 數字 + 符號** 皆須至少一個 |
| 後端 | `ValidatePasswordPolicy` → 不符回 **400003** + `domain.PasswordPolicyEN` |
| 前端 | `passwordPolicy.ts` + 語言包 `password.policy.hint` / `api.err.400003` |

**同一句說明：**

- EN: `Password must be at least 12 characters with upper, lower, digit, and symbol`
- ZH: `密碼須至少 12 碼，且含大寫、小寫、數字與符號`

前端依 code 400003 顯示語系文案；placeholder／hint／錯誤都用同一 key，勿另寫一句。

### 寄信（信箱驗證 / 重設密碼）

業務在 `internal/logic/auth/*`：產碼 → `Notification.RenderVerify` → `SendEmail`。  
寄送實作在 `internal/module/notification`（SMTP，Mailgun 相容）。

#### 模板與多語

- 引擎與 stand-alone 相同：**hermes**（`github.com/matcornic/hermes/v2`）
- 語系：`zh-TW`（預設）／`en`；來源為會員 `preferred_language`，忘記密碼則用請求 `language`（FE 當前 locale）
- Logo：`Brand.LogoURL` 或預設 `{PublicWebBase}/brand-mark.jpg`（`apps/web/public/brand-mark.jpg`）

```yaml
PublicWebBase: "http://127.0.0.1:5173"
Brand:
  Name: "Harbor Desk"
  LogoURL: ""   # 空 = PublicWebBase/brand-mark.jpg
  Copyright: "© Harbor Desk"
```

#### 本機：Mailpit（推薦）

```bash
# 已含在 old/infra docker-compose（mongo/redis/mailpit）
cd old/infra && docker compose --env-file ../deploy/.env.dev up -d mailpit

# UI 看信
open http://127.0.0.1:8025
```

| 項目 | 值 |
|------|-----|
| SMTP | `127.0.0.1:1025`（無密碼也可；可開 AUTH accept-any 模擬 Mailgun） |
| Web UI | http://127.0.0.1:8025 |
| gateway 預設 | `Mail.SMTP.Host=127.0.0.1` `Port=1025` |

呼叫 `POST /api/v1/auth/email/send-code` 或 `password/request-reset` 後，到 Mailpit UI 看 HTML 信。
#### 正式：Mailgun SMTP

```yaml
Mail:
  Sender: "Harbor Desk <noreply@your-domain.com>"
  SMTP:
    Host: "smtp.mailgun.org"
    Port: 587
    User: "postmaster@YOUR_DOMAIN"
    Password: "<Mailgun SMTP password>"
  DevExposeCode: false
```

同一套 `Mail.SMTP` 設定；本機指 Mailpit、上線指 Mailgun，**code 不用改**。

env 覆寫：`MAIL_SMTP_*` 或 `HAIXUN_SMTP_*`（與 `old/deploy/.env.dev` 相容）。

| SMTP Host | 行為 |
|-----------|------|
| **空** | log-only，不真寄 |
| **Mailpit / Mailgun** | 真走 SMTP 投遞 |

### 前後端串不起來？檢查這幾項

1. **gateway 有在跑** `ss -tlnp | grep 8888`；沒有就 `go run . -f etc/gateway.yaml`
2. **Mongo URI 有帳密**（`export MONGO_URI=...`；沒帶會查不到 seed）
3. **Redis 有密碼**（`export REDIS_PASSWORD=...`；沒帶 monc 回 `NOAUTH` → API 500）
4. **已跑 migrate**（`make migrate-up`，seed `admin@haixun.local`）
5. **FE 切到 live**：設定 → 資料來源 → Live（預設是 **mock**，根本不會打後端）
6. **API base**：`apps/web/.env` 建議 `VITE_API_BASE=`（空），走 Vite proxy `/api` → `:8888`  
   遠端開 `*.30cm.net` 時**不要**寫死 `http://127.0.0.1:8888`（那會打到使用者自己電腦）
7. 切 live 後**重新登入** seed：`admin@haixun.local` / `admin123`

| 資料 | 存哪 | 關機 |
|------|------|------|
| 會員 / settings | **Mongo**（monc） | 保留 |
| refresh token / 驗證碼 | **Mongo**（TTL index） | 保留（過期自動清） |
| monc 實體快取 | Redis（CacheRedis） | 可丟，會從 Mongo 回填 |

## 目錄

```text
generate/api/           # goctl 契約
generate/database/mongo # golang-migrate JSON
generate/goctl/         # handler 模板（102000）
cmd/worker/             # 獨立 worker
internal/handler|logic  # goctl 產 / 薄邏輯
internal/module/        # domain · repository · usecase
internal/lib/mongo      # monc URI 小工具
etc/gateway.yaml        # 唯一設定
```

## 設定摘要

```yaml
CacheRedis:           # monc Redis 快取
  - Host: 127.0.0.1:6379
    Type: node
Mongo:
  URI: "mongodb://127.0.0.1:27017"   # 有 auth 用 MONGO_URI 覆寫
  Database: "haixun"
```

### 會員 uid

- 從 **1_000_000** 起（seed admin = `1000000`）
- 顯示補成 **8 位**：`01000000`（`%08d` / FE `padStart(8,"0")`）
- 新註冊：`1000001`, `1000002`, …
