fix frontend

This commit is contained in:
王性驊 2026-07-10 12:54:45 +00:00
parent c57987f6a8
commit fcc76911bb
299 changed files with 19176 additions and 2681 deletions

3
.gitignore vendored
View File

@ -35,3 +35,6 @@ node_modules/
# --- OS / editor ---
.DS_Store
.cursor/
# deploy runtime
deploy/run/

View File

@ -3,7 +3,9 @@
## 封存與主線
- **`old/`**:舊系統整包封存,**只讀參考**。不要在 `old/` 修功能當交付。
- **新碼**:只寫在 repo 根下新目錄(`apps/`、`services/` 等,以 `docs/product` 為準)。
- **新碼**:只寫在 **`apps/`** 下(前端 `apps/web`、後端 `apps/backend` 等,以 `docs/product` 為準)。
- **後端落點**Phase C 巡樓 APIworker 在 **`apps/backend`**go-zero`stand-alone-service` 遷入改造)。
- **後端寫法(硬性)**:一律 **goctl**:改 `generate/api/*.api``make gen-api` → 業務只寫 `internal/logic/**``internal/module/**`。**禁止**手寫 `handler``routes.go` 當主路徑。
- **產品真相**`docs/product/<slug>/`requirements → spec → plan → tasks
- **流程**`/spec-driven``.grok/skills/spec-driven/`)。

View File

@ -38,6 +38,16 @@ npm run dev # 本機開發,監聽 0.0.0.0:5173區網可連
npm run build # 型別檢查 + 生產建置
```
## 部署dev 一鍵清乾淨重裝)
見 [`deploy/README.md`](./deploy/README.md)。
```bash
cp deploy/.env.example deploy/.env.dev # 首次:填密鑰
./deploy/scripts/redeploy.sh --yes --kill-orphan
# 可選 nginx./deploy/scripts/install-nginx.sh
```
產品任務進度:`docs/product/haixun-console/tasks/INDEX.md`(目前從 T001 起)。
## 開發流程

6
apps/backend/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/bin/
*.exe
*.test
coverage.out
.data/
uploads/

View File

@ -0,0 +1,7 @@
# 遷入說明(精簡)
- 骨架參考 `stand-alone-service`go-zero module 分層、lib/mongo 精神)。
- **不**整包 rsync 電商與 `code.30cm.net` 私有依賴。
- API`generate/api` → goctl。
- DB`generate/database/mongo` → golang-migrate。
- 執行體:`gateway` + `cmd/worker` only。

27
apps/backend/Makefile Normal file
View File

@ -0,0 +1,27 @@
# 只保留會重複打的指令;其餘直接 go / migrate。
.PHONY: build test gen-api tidy migrate-up migrate-down
build:
go build -o bin/gateway .
go build -o bin/worker ./cmd/worker
test:
go test ./...
# 改 generate/api/*.api 後執行handler 勿手寫
gen-api:
goctl api go -api generate/api/gateway.api -dir . -style go_zero --home generate/goctl
tidy:
go mod tidy
# DB檔案在 generate/database/mongo需本機已裝 migratemongodb tag
# 例MONGO_URL=mongodb://127.0.0.1:27017/haixun
MONGO_URL ?= mongodb://127.0.0.1:27017/haixun
migrate-up:
migrate -path generate/database/mongo -database "$(MONGO_URL)" up
migrate-down:
migrate -path generate/database/mongo -database "$(MONGO_URL)" down 1

193
apps/backend/README.md Normal file
View File

@ -0,0 +1,193 @@
# apps/backend
Harbor Desk Phase Cgo-zero
## 原則
| 做 | 不做 |
|----|------|
| `generate/api` + `make gen-api` | 手寫 handlerroutes |
| `generate/database/mongo` + `make migrate-up` | `cmd/init`、`cmd/seeder` 程式遷移 |
| `monc` + `CacheRedis` | 自己半套 mongo cache |
| `internal/logic` + `module/*/domain|repository|usecase` | 業務塞進 handler |
## 指令
```bash
cd apps/backend
make build # gateway + worker
make test
make gen-api # 改 .api 後
make migrate-up # 改 generate/database 後
make tidy
```
```bash
# 需要 Mongo + Redisgo-zero monc / redis
# Mongo 若有帳密(本機 infra 常見),一定要帶進 URI
export MONGO_URI='mongodb://USER:PASS@127.0.0.1:27017/?authSource=admin'
export MONGO_URL="$MONGO_URI" # migrate 用
make migrate-up
go run . -f etc/gateway.yaml
# admin@haixun.local / admin123seeduid=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** |
| 後端 | MinIOdev或 S3prodS3 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 顯示語系文案placeholderhint錯誤都用同一 key勿另寫一句。
### 寄信(信箱驗證 / 重設密碼)
業務在 `internal/logic/auth/*`:產碼 → `Notification.RenderVerify``SendEmail`
寄送實作在 `internal/module/notification`SMTPMailgun 相容)。
#### 模板與多語
- 引擎與 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-composemongo/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 實體快取 | RedisCacheRedis | 可丟,會從 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`, …

View File

@ -0,0 +1,55 @@
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"apps/backend/internal/config"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/logx"
)
// Independent worker binary (A-03). M0 stub: config load + heartbeat log only.
// Outbox/job runners land in M2/M4 (T133/T169).
func main() {
configFile := flag.String("f", "etc/gateway.yaml", "config file")
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
c.ApplyEnv()
workerID := c.Worker.ID
if workerID == "" {
workerID = "worker-1"
}
interval := time.Duration(c.Worker.PollIntervalMs) * time.Millisecond
if interval <= 0 {
interval = 2 * time.Second
}
logx.Infof("haixun worker stub started id=%s interval=%s (no publish yet)", workerID, interval)
fmt.Printf("worker stub running id=%s (Ctrl+C to stop)\n", workerID)
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
tick := time.NewTicker(interval)
defer tick.Stop()
for {
select {
case <-sig:
logx.Infof("worker %s shutting down", workerID)
return
case t := <-tick.C:
// Health heartbeat only — no job/outbox processing in M0.
logx.Infof("worker %s heartbeat at %s", workerID, t.UTC().Format(time.RFC3339))
}
}
}

View File

@ -0,0 +1,108 @@
# 巡樓 gateway — 唯一主設定檔A-02
# Secret 請用環境變數覆寫。
Name: haixun-gateway
Host: 0.0.0.0
Port: 8888
Timeout: 30000
MaxBytes: 104857600
Log:
Mode: console
Level: info
# go-zero monc 快取Redis— 與 monc.MustNewModel / CacheRedis 對接
# 若 Redis 有 requirepass設 Pass 或環境變數 REDIS_PASSWORD / HAIXUN_REDIS_PASSWORD
# (沒帶密碼會 NOAUTH登入 API 回 500
CacheRedis:
- Host: 127.0.0.1:6379
Type: node
Pass: ""
# Mongo — mon/monc 使用完整 URI含帳密時寫在 URI可用環境變數 MONGO_URI 覆寫)
# 本機有 auth 的例子:
# export MONGO_URI='mongodb://USER:PASS@127.0.0.1:27017/?authSource=admin'
# 沒設帳密但 Mongo 開了 auth → 查詢會失敗,登入會 500 而不是「密碼錯」
Mongo:
URI: "mongodb://127.0.0.1:27017"
Database: "haixun"
Auth:
AccessSecret: "change-me-access-secret"
AccessExpire: 86400
RefreshSecret: "change-me-refresh-secret"
RefreshExpire: 604800
Platform:
AIKey: ""
ExaKey: ""
ThreadsAppId: ""
ThreadsAppSecret: ""
Worker:
ID: "worker-local-1"
PollIntervalMs: 2000
Bcrypt:
Cost: 10
# OAuth空則 callback 走 mock / 開發路徑)
OAuth:
GoogleClientID: ""
LineClientID: ""
LineClientSecret: ""
LineRedirectURI: ""
# 驗證碼/重設密碼寄信
#
# 本機MailpitMailgun 相容 SMTP 介面):
# Host: 127.0.0.1 Port: 1025 User/Password 可空
# UI: http://127.0.0.1:8025
# 容器old/infra docker-compose 的 mailpit 服務
#
# 正式 Mailgun SMTP 例:
# Host: smtp.mailgun.org Port: 587
# User: postmaster@YOUR_DOMAIN Password: <API SMTP password>
# DevExposeCode: false
#
# env 覆寫MAIL_SMTP_* 或 HAIXUN_SMTP_*(舊名相容)
Mail:
Sender: "Harbor Desk <noreply@haixun.local>"
SMTP:
Host: "127.0.0.1"
Port: 1025
User: ""
Password: ""
# 不再於 API 回傳驗證碼碼只在郵件MailpitSMTP
DevExposeCode: false
# 前端 origin重設信連結 + 預設 logo URL/brand-mark.jpg
# 遠端 devhttps://threads-tool-dev.30cm.net
PublicWebBase: "http://127.0.0.1:5173"
# 郵件品牌hermes 模板LogoURL 空 = PublicWebBase/brand-mark.jpg
Brand:
Name: "Harbor Desk"
LogoURL: ""
Copyright: "© Harbor Desk"
# 物件儲存(頭像等)— **只接 MinIO/S3**
#
# Endpoint = gateway 上傳用(本機 MinIO一定要 :9000
# PublicBaseURL = 瀏覽器讀圖用(走 nginx :80不要寫 port、不要加 /bucket
# 成品網址 = PublicBaseURL + "/" + key
# 例http://10.0.0.5/haixun-assets/avatar/1000000/xxx.png
#
# Secret 建議用 env不要 commit
# export HAIXUN_STORAGE_S3_ACCESS_KEY=haixun
# export HAIXUN_STORAGE_S3_SECRET_KEY='你的密碼'
# 或 MINIO_ROOT_USER / MINIO_ROOT_PASSWORDApplyEnv 會讀)
ObjectStorage:
Endpoint: "http://127.0.0.1:9000"
Region: "us-east-1"
Bucket: "haixun-assets"
AccessKey: "haixun"
SecretKey: ""
UsePathStyle: true
# env HAIXUN_STORAGE_S3_PUBLIC_BASE_URL 會覆寫;勿用 /api/v1/public/assets新後端無此 route
PublicBaseURL: "https://threads-tool-dev.30cm.net/haixun-assets"

46
apps/backend/gateway.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"flag"
"fmt"
"os"
"apps/backend/internal/config"
"apps/backend/internal/handler"
"apps/backend/internal/lib/securelog"
"apps/backend/internal/middleware"
"apps/backend/internal/svc"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest"
)
// Gateway entry (go-zero). API from goctl; DB schema from generate/database migrate.
func main() {
// 必須在 mon/monc 建立前呼叫:否則 Mongo ReplaceOne 會把 password_hash 整包打進 log
securelog.SilenceMongo()
configFile := flag.String("f", "etc/gateway.yaml", "config file")
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
c.ApplyEnv()
// 禁止 Verbose否則可能印 request body 含 password
c.RestConf.Verbose = false
server := rest.MustNewServer(c.RestConf,
rest.WithNotAllowedHandler(middleware.NewCorsMiddleware().Handler()),
)
defer server.Stop()
server.Use(middleware.NewCorsMiddleware().Handle)
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
logx.Infof("haixun gateway %s:%d pid=%d", c.Host, c.Port, os.Getpid())
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}

View File

@ -0,0 +1,100 @@
syntax = "v1"
type (
AdminListMembersReq {
Page int `form:"page,optional"`
PageSize int `form:"pageSize,optional"`
Query string `form:"query,optional"`
Status string `form:"status,optional"`
}
AdminUidPath {
Uid string `path:"uid"`
}
AdminCreateMemberReq {
DisplayName string `json:"display_name"`
Email string `json:"email"`
Phone string `json:"phone,optional"`
Password string `json:"password,optional"`
Roles []string `json:"roles,optional"`
EmailVerified bool `json:"email_verified,optional"`
Bio string `json:"bio,optional"`
Nickname string `json:"nickname,optional"`
FullName string `json:"full_name,optional"`
}
AdminCreateMemberData {
User MemberPublic `json:"user"`
TemporaryPassword string `json:"temporary_password"`
}
AdminSuspendReq {
Uid string `path:"uid"`
Suspended bool `json:"suspended"`
}
AdminRolesReq {
Uid string `path:"uid"`
Roles []string `json:"roles"`
}
AdminEmailVerifiedReq {
Uid string `path:"uid"`
Verified bool `json:"verified"`
}
AdminResetPasswordReq {
Uid string `path:"uid"`
NewPassword string `json:"new_password,optional"`
Password string `json:"password,optional"`
}
AdminStatusReq {
Uid string `path:"uid"`
Status string `json:"status"`
}
AdminListData {
List []MemberPublic `json:"list"`
Pagination Pagination `json:"pagination"`
}
AdminMemberIdentitiesData {
List []IdentityPublic `json:"list"`
}
)
@server (
group: admin
prefix: /api/v1/admin
middleware: AuthJWT,AdminAuth
)
service gateway {
@handler ListMembers
get /members (AdminListMembersReq) returns (AdminListData)
@handler CreateMember
post /members (AdminCreateMemberReq) returns (AdminCreateMemberData)
@handler GetMember
get /members/:uid (AdminUidPath) returns (MemberPublic)
@handler SetSuspended
post /members/:uid/suspend (AdminSuspendReq) returns (MemberPublic)
@handler SetRoles
post /members/:uid/roles (AdminRolesReq) returns (MemberPublic)
@handler SetEmailVerified
post /members/:uid/email-verified (AdminEmailVerifiedReq) returns (MemberPublic)
@handler AdminResetPassword
post /members/:uid/reset-password (AdminResetPasswordReq) returns (AdminCreateMemberData)
@handler SetStatus
post /members/:uid/status (AdminStatusReq) returns (MemberPublic)
@handler ListMemberIdentities
get /members/:uid/identities (AdminUidPath) returns (AdminMemberIdentitiesData)
}

View File

@ -0,0 +1,195 @@
syntax = "v1"
type (
AuthLoginReq {
Email string `json:"email,optional"`
LoginId string `json:"login_id,optional"`
Password string `json:"password"`
Platform string `json:"platform,optional"` // platform|google|line (default platform)
}
AuthRegisterReq {
Email string `json:"email"`
Password string `json:"password"`
DisplayName string `json:"display_name,optional"`
Phone string `json:"phone,optional"`
}
AuthRefreshReq {
RefreshToken string `json:"refresh_token"`
}
AuthLogoutReq {
RefreshToken string `json:"refresh_token,optional"`
}
TokenPair {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
AuthSessionData {
Tokens TokenPair `json:"tokens"`
Member MemberPublic `json:"member"`
}
AuthTokensOnly {
Tokens TokenPair `json:"tokens"`
}
AuthProfilePatchReq {
DisplayName string `json:"display_name,optional"`
Nickname string `json:"nickname,optional"`
FullName string `json:"full_name,optional"`
Email string `json:"email,optional"`
Phone string `json:"phone,optional"`
Bio string `json:"bio,optional"`
Timezone string `json:"timezone,optional"`
NotifyEmail *bool `json:"notify_email,optional"`
// *string省略=不改;""=清除頭像http(s)=設定(禁止 data URL
AvatarUrl *string `json:"avatar_url,optional"`
GenderCode *int `json:"gender_code,optional"`
Birthdate *int64 `json:"birthdate,optional"`
National string `json:"national,optional"`
Address string `json:"address,optional"`
PostCode string `json:"post_code,optional"`
PreferredLanguage string `json:"preferred_language,optional"`
Currency string `json:"currency,optional"`
CurrentPassword string `json:"current_password,optional"`
NewPassword string `json:"new_password,optional"`
}
AuthRequestResetReq {
Email string `json:"email"`
// FE locale: zh-TW | en | en-US未登入時用有會員則優先 preferred_language
Language string `json:"language,optional"`
}
AuthRequestResetData {
Ok bool `json:"ok"`
Message string `json:"message"`
MockCode string `json:"mock_code,optional"`
MockResetPath string `json:"mock_reset_path,optional"`
}
AuthResetPasswordReq {
Email string `json:"email"`
Token string `json:"token,optional"`
Code string `json:"code,optional"`
NewPassword string `json:"new_password"`
}
AuthSendCodeData {
Ok bool `json:"ok"`
Message string `json:"message"`
MockCode string `json:"mock_code,optional"`
}
AuthVerifyEmailReq {
Code string `json:"code"`
}
AuthOAuthProviderPath {
Provider string `path:"provider"`
}
AuthOAuthCallbackReq {
Provider string `path:"provider"`
Code string `json:"code,optional"`
IdToken string `json:"id_token,optional"`
AccessToken string `json:"access_token,optional"`
RedirectUri string `json:"redirect_uri,optional"`
}
AuthOAuthStartData {
AuthorizeUrl string `json:"authorize_url"`
Provider string `json:"provider"`
State string `json:"state"`
}
AuthBindReq {
LoginId string `json:"login_id"`
Platform string `json:"platform"`
AccountType string `json:"account_type,optional"`
Password string `json:"password,optional"`
}
AuthUnbindReq {
LoginId string `json:"login_id"`
Platform string `json:"platform"`
}
AuthIdentitiesData {
List []IdentityPublic `json:"list"`
}
AuthLogoutAllData {
Ok bool `json:"ok"`
}
)
@server (
group: auth
prefix: /api/v1/auth
)
service gateway {
@handler Login
post /login (AuthLoginReq) returns (AuthSessionData)
@handler Logout
post /logout (AuthLogoutReq) returns (OkData)
@handler Refresh
post /refresh (AuthRefreshReq) returns (AuthTokensOnly)
@handler Register
post /register (AuthRegisterReq) returns (AuthSessionData)
@handler RequestPasswordReset
post /password/request-reset (AuthRequestResetReq) returns (AuthRequestResetData)
@handler ResetPassword
post /password/reset (AuthResetPasswordReq) returns (OkData)
@handler OAuthStart
post /oauth/:provider/start (AuthOAuthProviderPath) returns (AuthOAuthStartData)
@handler OAuthCallback
post /oauth/:provider/callback (AuthOAuthCallbackReq) returns (AuthSessionData)
}
@server (
group: auth
prefix: /api/v1/auth
middleware: AuthJWT
)
service gateway {
@handler Me
get /me returns (MemberPublic)
@handler UpdateProfile
patch /profile (AuthProfilePatchReq) returns (MemberPublic)
@handler UpdateProfilePut
put /profile (AuthProfilePatchReq) returns (MemberPublic)
@handler SendEmailCode
post /email/send-code returns (AuthSendCodeData)
@handler VerifyEmail
post /email/verify (AuthVerifyEmailReq) returns (MemberPublic)
@handler ListIdentities
get /identities returns (AuthIdentitiesData)
@handler BindAccount
post /bind (AuthBindReq) returns (IdentityPublic)
@handler UnbindAccount
post /unbind (AuthUnbindReq) returns (OkData)
@handler LogoutAll
post /logout-all returns (AuthLogoutAllData)
}

View File

@ -0,0 +1,57 @@
syntax = "v1"
type Empty {}
type OkData {
Ok bool `json:"ok"`
Message string `json:"message,optional"`
}
type Pagination {
Page int `json:"page"`
PageSize int `json:"pageSize"`
Total int64 `json:"total"`
TotalPages int `json:"totalPages"`
}
// IdentityPublic — login channel bound to member
type IdentityPublic {
LoginId string `json:"login_id"`
Platform string `json:"platform"`
AccountType string `json:"account_type"`
CreatedAt int64 `json:"created_at,optional"`
}
// MemberPublic — full profile (stand-alone user_info + Harbor Desk fields)
type MemberPublic {
TenantId string `json:"tenant_id"`
Uid int64 `json:"uid"`
Email string `json:"email"`
Phone string `json:"phone,optional"`
DisplayName string `json:"display_name"`
Nickname string `json:"nickname,optional"`
FullName string `json:"full_name,optional"`
Roles []string `json:"roles"`
Status string `json:"status"`
EmailVerified bool `json:"email_verified"`
EmailVerifiedAt int64 `json:"email_verified_at,optional"`
PhoneVerified bool `json:"phone_verified,optional"`
PhoneVerifiedAt int64 `json:"phone_verified_at,optional"`
InviteCode string `json:"invite_code,optional"`
ParentUid int64 `json:"parent_uid,optional"`
Bio string `json:"bio,optional"`
Timezone string `json:"timezone,optional"`
AvatarUrl string `json:"avatar_url,optional"`
NotifyEmail bool `json:"notify_email,optional"`
GenderCode int `json:"gender_code,optional"`
Birthdate int64 `json:"birthdate,optional"`
National string `json:"national,optional"`
Address string `json:"address,optional"`
PostCode string `json:"post_code,optional"`
PreferredLanguage string `json:"preferred_language,optional"`
Currency string `json:"currency,optional"`
Identities []IdentityPublic `json:"identities,optional"`
JoinedAt int64 `json:"joined_at,optional"`
CreatedAt int64 `json:"created_at,optional"`
UpdatedAt int64 `json:"updated_at,optional"`
}

View File

@ -0,0 +1,15 @@
syntax = "v1"
info (
title: "haixun-gateway"
desc: "Harbor Desk Phase C live API (goctl)"
author: "haixun"
version: "0.2.0"
)
import "common.api"
import "ping.api"
import "auth.api"
import "admin.api"
import "settings.api"
import "media.api"

View File

@ -0,0 +1,25 @@
syntax = "v1"
type (
MediaUploadReq {
// content: data URL (data:image/jpeg;base64,...) 或純 base64
Content string `json:"content"`
// kind: avatar | other決定 object path 前綴)
Kind string `json:"kind,optional"`
}
MediaUploadData {
Url string `json:"url"`
Key string `json:"key,optional"`
}
)
@server (
group: media
prefix: /api/v1/media
middleware: AuthJWT
)
service gateway {
@handler Upload
post /upload (MediaUploadReq) returns (MediaUploadData)
}

View File

@ -0,0 +1,18 @@
syntax = "v1"
type HealthData {
Status string `json:"status"`
Service string `json:"service"`
}
@server (
group: ping
prefix: /api/v1
)
service gateway {
@handler Ping
get /ping returns (HealthData)
@handler Health
get /health returns (HealthData)
}

View File

@ -0,0 +1,71 @@
syntax = "v1"
type (
AiSettingsData {
Provider string `json:"provider"`
Model string `json:"model"`
ResearchProvider string `json:"research_provider"`
ResearchModel string `json:"research_model"`
ApiKeyConfigured bool `json:"api_key_configured"`
ResearchApiKeyConfigured bool `json:"research_api_key_configured"`
PlatformKeyAvailable bool `json:"platform_key_available"`
}
SaveAiReq {
Provider string `json:"provider,optional"`
Model string `json:"model,optional"`
ResearchProvider string `json:"research_provider,optional"`
ResearchModel string `json:"research_model,optional"`
ApiKey string `json:"api_key,optional"`
ResearchApiKey string `json:"research_api_key,optional"`
ApiKeyConfigured *bool `json:"api_key_configured,optional"`
ResearchApiKeyConfigured *bool `json:"research_api_key_configured,optional"`
}
PlacementSettingsData {
WebSearchProvider string `json:"web_search_provider"`
ExpandStrategy string `json:"expand_strategy"`
BraveApiKeyConfigured bool `json:"brave_api_key_configured"`
ExaApiKeyConfigured bool `json:"exa_api_key_configured"`
DevModeEnabled bool `json:"dev_mode_enabled"`
PlatformKeyAvailable bool `json:"platform_key_available"`
}
SavePlacementReq {
ExpandStrategy string `json:"expand_strategy,optional"`
DevModeEnabled *bool `json:"dev_mode_enabled,optional"`
ExaApiKey string `json:"exa_api_key,optional"`
ExaApiKeyConfigured *bool `json:"exa_api_key_configured,optional"`
}
ListModelsReq {
Provider string `form:"provider,optional"`
}
ListModelsData {
List []string `json:"list"`
Provider string `json:"provider"`
}
)
@server (
group: settings
prefix: /api/v1/settings
middleware: AuthJWT
)
service gateway {
@handler GetAi
get /ai returns (AiSettingsData)
@handler SaveAi
put /ai (SaveAiReq) returns (AiSettingsData)
@handler GetPlacement
get /placement returns (PlacementSettingsData)
@handler SavePlacement
put /placement (SavePlacementReq) returns (PlacementSettingsData)
@handler ListModels
get /models (ListModelsReq) returns (ListModelsData)
}

View File

@ -0,0 +1,64 @@
# Database migrationsgolang-migrate + MongoDB
**第一性原理:** indexseed 用 **檔案** 宣告,交給 `migrate`;沒有 Go seederinit 程式。
工具:[golang-migrate](https://github.com/golang-migrate/migrate)
Mongo driver 讀的是 **JSON**`db.runCommand` 陣列),不是 shell `.js`
## 目錄
```text
generate/database/mongo/
000001_members_indexes.up.json
000001_members_indexes.down.json
000002_jobs_indexes.up.json
...
000003_seed_admin.up.json # 可選種子
```
命名:`{version}_{name}.up.json` / `.down.json`
## 指令
```bash
# 安裝 CLI一次
go install -tags 'mongodb' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# 連線字串要含 database 名
export MONGO_URL='mongodb://127.0.0.1:27017/haixun'
# 套用全部
migrate -path generate/database/mongo -database "$MONGO_URL" up
# 回滾一步
migrate -path generate/database/mongo -database "$MONGO_URL" down 1
# 看版本
migrate -path generate/database/mongo -database "$MONGO_URL" version
```
Makefile
```bash
make migrate-up
make migrate-down
```
## 何時寫什麼
| 類型 | 放哪 | 範例 |
|------|------|------|
| Index / collection 約束 | `*.up.json` createIndexes | members.uid unique |
| 種子資料 | 另開 version如 000003_seed_* | admin 帳 |
| 業務邏輯、密碼 runtime 產生 | **不要**寫進 migration | 用 API / 測試 helper |
種子 admin 預設:`admin@haixun.local` / `admin123`hash 寫死在 up 檔;正式環境請改或不要跑 seed migration
## 與 gateway
`make migrate-up`,再 `go run . -f etc/gateway.yaml`(需 Mongo + Redis
## 不要
- 手寫 Go seederinit
- 手寫 migration runner
- 把 stand-alone 電商 `.txt` 當 migrate 來源(格式不同;要用 JSON runCommand

View File

@ -0,0 +1,22 @@
[
{
"dropIndexes": "members",
"index": "uid_unique"
},
{
"dropIndexes": "members",
"index": "email_unique"
},
{
"dropIndexes": "members",
"index": "invite_code_1"
},
{
"dropIndexes": "members",
"index": "status_roles"
},
{
"dropIndexes": "member_settings",
"index": "settings_uid_unique"
}
]

View File

@ -0,0 +1,45 @@
[
{
"createIndexes": "members",
"indexes": [
{
"key": { "uid": 1 },
"name": "uid_unique",
"unique": true
},
{
"key": { "email": 1 },
"name": "email_unique",
"unique": true
},
{
"key": { "invite_code": 1 },
"name": "invite_code_1",
"sparse": true
},
{
"key": { "status": 1, "roles": 1 },
"name": "status_roles"
}
]
},
{
"createIndexes": "member_settings",
"indexes": [
{
"key": { "uid": 1 },
"name": "settings_uid_unique",
"unique": true
}
]
},
{
"createIndexes": "counters",
"indexes": [
{
"key": { "_id": 1 },
"name": "_id_"
}
]
}
]

View File

@ -0,0 +1,10 @@
[
{
"dropIndexes": "jobs",
"index": "owner_created"
},
{
"dropIndexes": "jobs",
"index": "status_1"
}
]

View File

@ -0,0 +1,15 @@
[
{
"createIndexes": "jobs",
"indexes": [
{
"key": { "owner_uid": 1, "created_at": -1 },
"name": "owner_created"
},
{
"key": { "status": 1 },
"name": "status_1"
}
]
}
]

View File

@ -0,0 +1,11 @@
[
{
"delete": "members",
"deletes": [
{
"q": { "uid": 1, "email": "admin@30cm.net" },
"limit": 1
}
]
}
]

View File

@ -0,0 +1,36 @@
[
{
"insert": "members",
"documents": [
{
"uid": 1000000,
"tenant_id": "default",
"email": "admin@30cm.net",
"display_name": "Harbor Admin",
"roles": ["admin", "member"],
"status": "active",
"email_verified": true,
"email_verified_at": 1700000000000000000,
"invite_code": "SEEDADMIN",
"password_hash": "$2a$10$iGGFNC5rM8MFhU8Wj6a7TOcQCsTeD03bkEODvQBh2ZxvCH.PcLl3O",
"timezone": "Asia/Taipei",
"notify_email": false,
"bio": "",
"created_at": 1700000000000000000,
"updated_at": 1700000000000000000,
"seed_note": "password is admin123 — change in production; uid=1000000 (8 digits from 1M)"
}
],
"ordered": false
},
{
"update": "counters",
"updates": [
{
"q": { "_id": "member_uid" },
"u": { "$setOnInsert": { "seq": 1000000 } },
"upsert": true
}
]
}
]

View File

@ -0,0 +1,14 @@
[
{
"dropIndexes": "refresh_tokens",
"index": "refresh_uid_1"
},
{
"dropIndexes": "refresh_tokens",
"index": "refresh_ttl"
},
{
"dropIndexes": "auth_codes",
"index": "auth_code_ttl"
}
]

View File

@ -0,0 +1,26 @@
[
{
"createIndexes": "refresh_tokens",
"indexes": [
{
"key": { "uid": 1 },
"name": "refresh_uid_1"
},
{
"key": { "expires_at": 1 },
"name": "refresh_ttl",
"expireAfterSeconds": 0
}
]
},
{
"createIndexes": "auth_codes",
"indexes": [
{
"key": { "expires_at": 1 },
"name": "auth_code_ttl",
"expireAfterSeconds": 0
}
]
}
]

View File

@ -0,0 +1,18 @@
[
{
"dropIndexes": "identities",
"index": "login_platform_unique"
},
{
"dropIndexes": "identities",
"index": "identities_uid_1"
},
{
"dropIndexes": "identities",
"index": "platform_account_type"
},
{
"dropIndexes": "members",
"index": "phone_1"
}
]

View File

@ -0,0 +1,30 @@
[
{
"createIndexes": "identities",
"indexes": [
{
"key": { "login_id": 1, "platform": 1 },
"name": "login_platform_unique",
"unique": true
},
{
"key": { "uid": 1 },
"name": "identities_uid_1"
},
{
"key": { "platform": 1, "account_type": 1 },
"name": "platform_account_type"
}
]
},
{
"createIndexes": "members",
"indexes": [
{
"key": { "phone": 1 },
"name": "phone_1",
"sparse": true
}
]
}
]

View File

@ -0,0 +1,11 @@
[
{
"delete": "identities",
"deletes": [
{
"q": { "uid": 1, "login_id": "admin@haixun.local", "platform": "platform" },
"limit": 1
}
]
}
]

View File

@ -0,0 +1,17 @@
[
{
"insert": "identities",
"documents": [
{
"uid": 1000000,
"login_id": "admin@haixun.local",
"platform": "platform",
"account_type": "email",
"password_hash": "$2a$10$iGGFNC5rM8MFhU8Wj6a7TOcQCsTeD03bkEODvQBh2ZxvCH.PcLl3O",
"created_at": 1700000000000000000,
"updated_at": 1700000000000000000
}
],
"ordered": false
}
]

View File

@ -0,0 +1,22 @@
[
{
"update": "members",
"updates": [
{
"q": { "uid": 1000000, "email": "admin@haixun.local" },
"u": { "$set": { "uid": 1 } },
"multi": false
}
]
},
{
"update": "identities",
"updates": [
{
"q": { "uid": 1000000, "login_id": "admin@haixun.local" },
"u": { "$set": { "uid": 1 } },
"multi": true
}
]
}
]

View File

@ -0,0 +1,57 @@
[
{
"update": "counters",
"updates": [
{
"q": { "_id": "member_uid", "seq": { "$lt": 1000000 } },
"u": { "$set": { "seq": 1000000 } },
"upsert": false
},
{
"q": { "_id": "member_uid" },
"u": { "$setOnInsert": { "seq": 1000000 } },
"upsert": true
}
]
},
{
"update": "members",
"updates": [
{
"q": { "uid": 1, "email": "admin@haixun.local" },
"u": { "$set": { "uid": 1000000 } },
"multi": false
}
]
},
{
"update": "identities",
"updates": [
{
"q": { "uid": 1, "login_id": "admin@haixun.local" },
"u": { "$set": { "uid": 1000000 } },
"multi": true
}
]
},
{
"update": "member_settings",
"updates": [
{
"q": { "uid": 1 },
"u": { "$set": { "uid": 1000000 } },
"multi": false
}
]
},
{
"update": "refresh_tokens",
"updates": [
{
"q": { "uid": 1 },
"u": { "$set": { "uid": 1000000 } },
"multi": true
}
]
}
]

View File

@ -0,0 +1,27 @@
// Code generated by goctl. DO NOT EDIT.
// goctl {{.version}}
package {{.PkgName}}
import (
"net/http"
"apps/backend/internal/response"
{{if .HasRequest}}"github.com/zeromicro/go-zero/rest/httpx"
{{end}}{{.ImportPackages}}
)
{{if .HasDoc}}{{.Doc}}{{end}}
func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
{{if .HasRequest}}var req types.{{.RequestType}}
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
{{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
{{if .HasResp}}data, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}})
{{if .HasResp}}response.Write(r.Context(), w, data, err){{else}}response.Write(r.Context(), w, nil, err){{end}}
}
}

96
apps/backend/go.mod Normal file
View File

@ -0,0 +1,96 @@
module apps/backend
go 1.25.0
require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/stretchr/testify v1.10.0
github.com/zeromicro/go-zero v1.7.6
go.mongodb.org/mongo-driver v1.17.1
golang.org/x/crypto v0.54.0
)
require (
github.com/Masterminds/semver v1.4.2 // indirect
github.com/Masterminds/sprig v2.16.0+incompatible // indirect
github.com/PuerkitoBio/goquery v1.5.0 // indirect
github.com/andybalholm/cascadia v1.0.0 // indirect
github.com/aokoli/goutils v1.0.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0 // indirect
github.com/aws/smithy-go v1.27.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/huandu/xstrings v1.2.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/matcornic/hermes/v2 v2.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/redis/go-redis/v9 v9.7.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 // indirect
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

261
apps/backend/go.sum Normal file
View File

@ -0,0 +1,261 @@
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY=
github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0=
github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14/go.mod h1:zwM6veDkhGgQFqkBy+uT28AAYpLu+uFMlPl+rCg/73E=
github.com/aws/aws-sdk-go-v2/credentials v1.19.28 h1:zTXJSsNcoO91/mTXsZoYf0AK8dvNPiA58/VtyGXR+wM=
github.com/aws/aws-sdk-go-v2/credentials v1.19.28/go.mod h1:Kd9E0JzDBW/q1xbsHFrev/GnbAf5J0Ng8xoyc7HZ91Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 h1:9Fjh6fi/U5JEStVZijmaMpUwE/gvBJj7x2B/PjbO9To=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23/go.mod h1:iMoT2f1tClxrWAAnKCXjZQ6LOmfLrMG14wmnWpM+F14=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 h1:uao4A3QZ5UmB326V6KF+qRpv9Tjz7IlnlnTbbANntlU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31/go.mod h1:I/1+z0VwL1GhQyLgkoHDlygpUZ+iTAwOQ/NsftiUL2I=
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0 h1:XptwLL+UHXgafYMIHTy59IRovLbhz3znkxY2uS/pbXU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do=
github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc=
github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 h1:L0rPdfzq43+NV8rfIx2kA4iSSLRj2jN5ijYHoeXRwvQ=
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe h1:9YnI5plmy+ad6BM+JCLJb2ZV7/TNiE5l7SNKfumYKgc=
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zeromicro/go-zero v1.7.6 h1:SArK4xecdrpVY3ZFJcbc0IZCx+NuWyHNjCv9f1+Gwrc=
github.com/zeromicro/go-zero v1.7.6/go.mod h1:SmGykRm5e0Z4CGNj+GaSKDffaHzQV56fel0FkymTLlE=
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY=
go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20181029175232-7e6ffbd03851/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=

View File

@ -0,0 +1,211 @@
package config
import (
"os"
"strings"
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/rest"
)
// Config — go-zero 風格RestConf + CacheRedis + Mongo URI.
type Config struct {
rest.RestConf
// CacheRedis is go-zero cache.CacheConf (feeds monc Redis 快取).
CacheRedis cache.CacheConf
Mongo struct {
URI string
Database string
}
Auth struct {
AccessSecret string
AccessExpire int64
RefreshSecret string
RefreshExpire int64
}
Platform struct {
AIKey string `json:",optional"`
ExaKey string `json:",optional"`
ThreadsAppId string `json:",optional"`
ThreadsAppSecret string `json:",optional"`
}
Worker struct {
ID string `json:",default=worker-1"`
PollIntervalMs int `json:",default=2000"`
}
// Bcrypt cost (stand-alone Bcrypt.Cost)
Bcrypt struct {
Cost int `json:",default=10"`
}
// OAuth third-party (optional; empty → mock path in callback)
OAuth struct {
GoogleClientID string `json:",optional"`
LineClientID string `json:",optional"`
LineClientSecret string `json:",optional"`
LineRedirectURI string `json:",optional"`
}
// Mail — 驗證碼重設密碼寄信SMTP空 Host 則只打 log + 可回 mock_code
Mail struct {
Sender string `json:",optional"` // e.g. "Harbor Desk <noreply@example.com>"
SMTP struct {
Host string `json:",optional"`
Port int `json:",default=587"`
User string `json:",optional"`
Password string `json:",optional"`
} `json:",optional"`
// DevExposeCode: API 回 mock_code本機沒信箱時用。正式請 false。
DevExposeCode bool `json:",default=true"`
} `json:",optional"`
// PublicWebBase — 前端 origin用於重設密碼信內連結logo勿尾斜線
// 例http://127.0.0.1:5173 或 https://threads-tool-dev.30cm.net
PublicWebBase string `json:",optional,default=http://127.0.0.1:5173"`
// Brand — 郵件 chromehermesLogoURL 空則用 PublicWebBase + /brand-mark.jpg
Brand struct {
Name string `json:",optional,default=Harbor Desk"`
LogoURL string `json:",optional"` // absolute URL
Copyright string `json:",optional,default=© Harbor Desk"`
} `json:",optional"`
// ObjectStorage — 頭像等二進位;**僅 MinIO / S3**(不落本機磁碟)
ObjectStorage struct {
Endpoint string `json:",optional"` // 必填e.g. http://127.0.0.1:9000
Region string `json:",optional,default=us-east-1"`
Bucket string `json:",optional,default=haixun-assets"`
AccessKey string `json:",optional"`
SecretKey string `json:",optional"`
UsePathStyle bool `json:",optional,default=true"`
PublicBaseURL string `json:",optional"` // 公開讀取前綴,勿尾斜線
} `json:",optional"`
}
// ApplyEnv overlays secrets from environment.
func (c *Config) ApplyEnv() {
if v := os.Getenv("MONGO_URI"); v != "" {
c.Mongo.URI = v
}
if v := os.Getenv("MONGO_DATABASE"); v != "" {
c.Mongo.Database = v
}
if v := os.Getenv("REDIS_HOST"); v != "" && len(c.CacheRedis) > 0 {
c.CacheRedis[0].RedisConf.Host = v
}
if len(c.CacheRedis) > 0 {
// monc 快取 Redis 帳密(本機 infra 常開 requirepass
if v := os.Getenv("REDIS_PASSWORD"); v != "" {
c.CacheRedis[0].RedisConf.Pass = v
} else if v := os.Getenv("HAIXUN_REDIS_PASSWORD"); v != "" {
c.CacheRedis[0].RedisConf.Pass = v
}
if v := os.Getenv("HAIXUN_REDIS_ADDR"); v != "" {
c.CacheRedis[0].RedisConf.Host = v
}
}
if v := os.Getenv("AUTH_ACCESS_SECRET"); v != "" {
c.Auth.AccessSecret = v
}
if v := os.Getenv("AUTH_REFRESH_SECRET"); v != "" {
c.Auth.RefreshSecret = v
}
if v := os.Getenv("PLATFORM_AI_KEY"); v != "" {
c.Platform.AIKey = v
}
if v := os.Getenv("PLATFORM_EXA_KEY"); v != "" {
c.Platform.ExaKey = v
}
if v := os.Getenv("WORKER_ID"); v != "" {
c.Worker.ID = v
}
if v := os.Getenv("PORT"); v != "" {
var p int
for _, ch := range v {
if ch < '0' || ch > '9' {
return
}
p = p*10 + int(ch-'0')
}
if p > 0 {
c.Port = p
}
}
// Mail / SMTPMAIL_* 優先HAIXUN_SMTP_* 與 old/infra 相容)
if v := firstEnv("MAIL_SENDER", "HAIXUN_SMTP_FROM"); v != "" {
c.Mail.Sender = v
}
if v := firstEnv("MAIL_SMTP_HOST", "HAIXUN_SMTP_HOST"); v != "" {
c.Mail.SMTP.Host = v
}
if v := firstEnv("MAIL_SMTP_USER", "HAIXUN_SMTP_USERNAME"); v != "" {
c.Mail.SMTP.User = v
}
if v := firstEnv("MAIL_SMTP_PASSWORD", "HAIXUN_SMTP_PASSWORD"); v != "" {
c.Mail.SMTP.Password = v
}
if v := firstEnv("MAIL_SMTP_PORT", "HAIXUN_SMTP_PORT"); v != "" {
if p := parsePort(v); p > 0 {
c.Mail.SMTP.Port = p
}
}
if v := os.Getenv("MAIL_DEV_EXPOSE_CODE"); v == "0" || v == "false" || v == "FALSE" {
c.Mail.DevExposeCode = false
} else if v == "1" || v == "true" || v == "TRUE" {
c.Mail.DevExposeCode = true
}
if v := firstEnv("PUBLIC_WEB_BASE", "HAIXUN_PUBLIC_WEB_BASE"); v != "" {
c.PublicWebBase = strings.TrimRight(v, "/")
}
if v := os.Getenv("MAIL_BRAND_NAME"); v != "" {
c.Brand.Name = v
}
if v := firstEnv("MAIL_LOGO_URL", "HAIXUN_MAIL_LOGO_URL"); v != "" {
c.Brand.LogoURL = v
}
if v := os.Getenv("MAIL_COPYRIGHT"); v != "" {
c.Brand.Copyright = v
}
// Object storage (HAIXUN_STORAGE_S3_* 與 old/infra 相容)
if v := firstEnv("OBJECT_STORAGE_ENDPOINT", "HAIXUN_STORAGE_S3_ENDPOINT"); v != "" {
c.ObjectStorage.Endpoint = v
}
if v := firstEnv("OBJECT_STORAGE_REGION", "HAIXUN_STORAGE_S3_REGION"); v != "" {
c.ObjectStorage.Region = v
}
if v := firstEnv("OBJECT_STORAGE_BUCKET", "HAIXUN_STORAGE_S3_BUCKET"); v != "" {
c.ObjectStorage.Bucket = v
}
if v := firstEnv("OBJECT_STORAGE_ACCESS_KEY", "HAIXUN_STORAGE_S3_ACCESS_KEY", "MINIO_ROOT_USER"); v != "" {
c.ObjectStorage.AccessKey = v
}
if v := firstEnv("OBJECT_STORAGE_SECRET_KEY", "HAIXUN_STORAGE_S3_SECRET_KEY", "MINIO_ROOT_PASSWORD"); v != "" {
c.ObjectStorage.SecretKey = v
}
if v := firstEnv("OBJECT_STORAGE_PUBLIC_BASE_URL", "HAIXUN_STORAGE_S3_PUBLIC_BASE_URL"); v != "" {
c.ObjectStorage.PublicBaseURL = strings.TrimRight(v, "/")
}
if v := os.Getenv("HAIXUN_STORAGE_S3_USE_PATH_STYLE"); v == "0" || v == "false" {
c.ObjectStorage.UsePathStyle = false
} else if v == "1" || v == "true" {
c.ObjectStorage.UsePathStyle = true
}
}
func firstEnv(keys ...string) string {
for _, k := range keys {
if v := os.Getenv(k); v != "" {
return v
}
}
return ""
}
func parsePort(v string) int {
var p int
for _, ch := range v {
if ch < '0' || ch > '9' {
return 0
}
p = p*10 + int(ch-'0')
}
return p
}

View File

@ -0,0 +1,20 @@
package domain
// SuccessCode is the project success envelope code (AGENTS.md / spec A-07).
// Migrated from stand-alone-service 10200 → unified 102000.
const SuccessCode int64 = 102000
const SuccessMessage = "success"
const TokenTypeBearer = "Bearer"
type ContextKey string
func (c ContextKey) String() string { return string(c) }
const (
UIDCode ContextKey = "uid"
RoleCode ContextKey = "role"
ScopeCode ContextKey = "scope"
DeviceIDCode ContextKey = "device_id"
)

View File

@ -0,0 +1,9 @@
package domain
import "testing"
func TestSuccessCode_Is102000(t *testing.T) {
if SuccessCode != 102000 {
t.Fatalf("SuccessCode=%d want 102000 (migrated from stand-alone 10200)", SuccessCode)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package admin
import (
"net/http"
"apps/backend/internal/logic/admin"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func AdminResetPasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminResetPasswordReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := admin.NewAdminResetPasswordLogic(r.Context(), svcCtx)
data, err := l.AdminResetPassword(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package admin
import (
"net/http"
"apps/backend/internal/logic/admin"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func CreateMemberHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminCreateMemberReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := admin.NewCreateMemberLogic(r.Context(), svcCtx)
data, err := l.CreateMember(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package admin
import (
"net/http"
"apps/backend/internal/logic/admin"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetMemberHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminUidPath
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := admin.NewGetMemberLogic(r.Context(), svcCtx)
data, err := l.GetMember(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package admin
import (
"net/http"
"apps/backend/internal/logic/admin"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ListMemberIdentitiesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminUidPath
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := admin.NewListMemberIdentitiesLogic(r.Context(), svcCtx)
data, err := l.ListMemberIdentities(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package admin
import (
"net/http"
"apps/backend/internal/logic/admin"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ListMembersHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminListMembersReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := admin.NewListMembersLogic(r.Context(), svcCtx)
data, err := l.ListMembers(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package admin
import (
"net/http"
"apps/backend/internal/logic/admin"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func SetEmailVerifiedHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminEmailVerifiedReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := admin.NewSetEmailVerifiedLogic(r.Context(), svcCtx)
data, err := l.SetEmailVerified(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package admin
import (
"net/http"
"apps/backend/internal/logic/admin"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func SetRolesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminRolesReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := admin.NewSetRolesLogic(r.Context(), svcCtx)
data, err := l.SetRoles(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package admin
import (
"net/http"
"apps/backend/internal/logic/admin"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func SetStatusHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminStatusReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := admin.NewSetStatusLogic(r.Context(), svcCtx)
data, err := l.SetStatus(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package admin
import (
"net/http"
"apps/backend/internal/logic/admin"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func SetSuspendedHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AdminSuspendReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := admin.NewSetSuspendedLogic(r.Context(), svcCtx)
data, err := l.SetSuspended(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func BindAccountHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthBindReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewBindAccountLogic(r.Context(), svcCtx)
data, err := l.BindAccount(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,20 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
)
func ListIdentitiesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := auth.NewListIdentitiesLogic(r.Context(), svcCtx)
data, err := l.ListIdentities()
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthLoginReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewLoginLogic(r.Context(), svcCtx)
data, err := l.Login(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,20 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
)
func LogoutAllHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := auth.NewLogoutAllLogic(r.Context(), svcCtx)
data, err := l.LogoutAll()
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func LogoutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthLogoutReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewLogoutLogic(r.Context(), svcCtx)
data, err := l.Logout(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,20 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
)
func MeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := auth.NewMeLogic(r.Context(), svcCtx)
data, err := l.Me()
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func OAuthCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthOAuthCallbackReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewOAuthCallbackLogic(r.Context(), svcCtx)
data, err := l.OAuthCallback(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func OAuthStartHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthOAuthProviderPath
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewOAuthStartLogic(r.Context(), svcCtx)
data, err := l.OAuthStart(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func RefreshHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthRefreshReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewRefreshLogic(r.Context(), svcCtx)
data, err := l.Refresh(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthRegisterReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewRegisterLogic(r.Context(), svcCtx)
data, err := l.Register(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func RequestPasswordResetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthRequestResetReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewRequestPasswordResetLogic(r.Context(), svcCtx)
data, err := l.RequestPasswordReset(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ResetPasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthResetPasswordReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewResetPasswordLogic(r.Context(), svcCtx)
data, err := l.ResetPassword(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,20 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
)
func SendEmailCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := auth.NewSendEmailCodeLogic(r.Context(), svcCtx)
data, err := l.SendEmailCode()
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func UnbindAccountHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthUnbindReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewUnbindAccountLogic(r.Context(), svcCtx)
data, err := l.UnbindAccount(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func UpdateProfileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthProfilePatchReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewUpdateProfileLogic(r.Context(), svcCtx)
data, err := l.UpdateProfile(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func UpdateProfilePutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthProfilePatchReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewUpdateProfilePutLogic(r.Context(), svcCtx)
data, err := l.UpdateProfilePut(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package auth
import (
"net/http"
"apps/backend/internal/logic/auth"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func VerifyEmailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AuthVerifyEmailReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := auth.NewVerifyEmailLogic(r.Context(), svcCtx)
data, err := l.VerifyEmail(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package media
import (
"net/http"
"apps/backend/internal/logic/media"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func UploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.MediaUploadReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := media.NewUploadLogic(r.Context(), svcCtx)
data, err := l.Upload(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,20 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package ping
import (
"net/http"
"apps/backend/internal/logic/ping"
"apps/backend/internal/response"
"apps/backend/internal/svc"
)
func HealthHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := ping.NewHealthLogic(r.Context(), svcCtx)
data, err := l.Health()
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,20 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package ping
import (
"net/http"
"apps/backend/internal/logic/ping"
"apps/backend/internal/response"
"apps/backend/internal/svc"
)
func PingHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := ping.NewPingLogic(r.Context(), svcCtx)
data, err := l.Ping()
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,237 @@
// Code generated by goctl. DO NOT EDIT.
// goctl 1.7.6
package handler
import (
"net/http"
admin "apps/backend/internal/handler/admin"
auth "apps/backend/internal/handler/auth"
media "apps/backend/internal/handler/media"
ping "apps/backend/internal/handler/ping"
settings "apps/backend/internal/handler/settings"
"apps/backend/internal/svc"
"github.com/zeromicro/go-zero/rest"
)
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthJWT, serverCtx.AdminAuth},
[]rest.Route{
{
Method: http.MethodGet,
Path: "/members",
Handler: admin.ListMembersHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/members",
Handler: admin.CreateMemberHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/members/:uid",
Handler: admin.GetMemberHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/members/:uid/email-verified",
Handler: admin.SetEmailVerifiedHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/members/:uid/identities",
Handler: admin.ListMemberIdentitiesHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/members/:uid/reset-password",
Handler: admin.AdminResetPasswordHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/members/:uid/roles",
Handler: admin.SetRolesHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/members/:uid/status",
Handler: admin.SetStatusHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/members/:uid/suspend",
Handler: admin.SetSuspendedHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/v1/admin"),
)
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodPost,
Path: "/login",
Handler: auth.LoginHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/logout",
Handler: auth.LogoutHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/oauth/:provider/callback",
Handler: auth.OAuthCallbackHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/oauth/:provider/start",
Handler: auth.OAuthStartHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/password/request-reset",
Handler: auth.RequestPasswordResetHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/password/reset",
Handler: auth.ResetPasswordHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/refresh",
Handler: auth.RefreshHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/register",
Handler: auth.RegisterHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1/auth"),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthJWT},
[]rest.Route{
{
Method: http.MethodPost,
Path: "/bind",
Handler: auth.BindAccountHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/email/send-code",
Handler: auth.SendEmailCodeHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/email/verify",
Handler: auth.VerifyEmailHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/identities",
Handler: auth.ListIdentitiesHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/logout-all",
Handler: auth.LogoutAllHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/me",
Handler: auth.MeHandler(serverCtx),
},
{
Method: http.MethodPatch,
Path: "/profile",
Handler: auth.UpdateProfileHandler(serverCtx),
},
{
Method: http.MethodPut,
Path: "/profile",
Handler: auth.UpdateProfilePutHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/unbind",
Handler: auth.UnbindAccountHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/v1/auth"),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthJWT},
[]rest.Route{
{
Method: http.MethodPost,
Path: "/upload",
Handler: media.UploadHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/v1/media"),
)
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/health",
Handler: ping.HealthHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/ping",
Handler: ping.PingHandler(serverCtx),
},
},
rest.WithPrefix("/api/v1"),
)
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthJWT},
[]rest.Route{
{
Method: http.MethodGet,
Path: "/ai",
Handler: settings.GetAiHandler(serverCtx),
},
{
Method: http.MethodPut,
Path: "/ai",
Handler: settings.SaveAiHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/models",
Handler: settings.ListModelsHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/placement",
Handler: settings.GetPlacementHandler(serverCtx),
},
{
Method: http.MethodPut,
Path: "/placement",
Handler: settings.SavePlacementHandler(serverCtx),
},
}...,
),
rest.WithPrefix("/api/v1/settings"),
)
}

View File

@ -0,0 +1,20 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package settings
import (
"net/http"
"apps/backend/internal/logic/settings"
"apps/backend/internal/response"
"apps/backend/internal/svc"
)
func GetAiHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := settings.NewGetAiLogic(r.Context(), svcCtx)
data, err := l.GetAi()
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,20 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package settings
import (
"net/http"
"apps/backend/internal/logic/settings"
"apps/backend/internal/response"
"apps/backend/internal/svc"
)
func GetPlacementHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := settings.NewGetPlacementLogic(r.Context(), svcCtx)
data, err := l.GetPlacement()
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package settings
import (
"net/http"
"apps/backend/internal/logic/settings"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func ListModelsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ListModelsReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := settings.NewListModelsLogic(r.Context(), svcCtx)
data, err := l.ListModels(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package settings
import (
"net/http"
"apps/backend/internal/logic/settings"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func SaveAiHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SaveAiReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := settings.NewSaveAiLogic(r.Context(), svcCtx)
data, err := l.SaveAi(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,28 @@
// Code generated by goctl. DO NOT EDIT.
// goctl <no value>
package settings
import (
"net/http"
"apps/backend/internal/logic/settings"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func SavePlacementHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SavePlacementReq
if err := httpx.Parse(r, &req); err != nil {
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
return
}
l := settings.NewSavePlacementLogic(r.Context(), svcCtx)
data, err := l.SavePlacement(&req)
response.Write(r.Context(), w, data, err)
}
}

View File

@ -0,0 +1,14 @@
package mongo
import "os"
// MustMongoURI returns Mongo URI from arg or MONGO_URI env (for mon/monc).
func MustMongoURI(uri string) string {
if v := os.Getenv("MONGO_URI"); v != "" {
return v
}
if uri == "" {
panic("mongo: empty URI (set Mongo.URI or MONGO_URI)")
}
return uri
}

View File

@ -0,0 +1,64 @@
// Package securelog centralizes log hygiene for secrets.
//
// Policy:
// - Never log password / new_password / current_password / temporary_password
// - Never log password_hash, refresh_token, access_token, api keys
// - go-zero mon dumps full Mongo docs at info — call SilenceMongo() at process start
package securelog
import (
"encoding/json"
"regexp"
"strings"
"github.com/zeromicro/go-zero/core/stores/mon"
)
// sensitiveJSONKeys matched case-insensitively as JSON object keys.
var sensitiveKeyPattern = regexp.MustCompile(
`(?i)("(?:password|new_password|current_password|temporary_password|password_hash|` +
`access_token|refresh_token|api_key|secret|authorization|cookie)"\s*:\s*)` +
`(?:"(?:\\.|[^"\\])*"|null|true|false|-?\d+(?:\.\d+)?)`,
)
// SilenceMongo disables go-zero mon command logging (including document dumps).
// monc uses mon under the hood; without this, ReplaceOne logs password_hash.
func SilenceMongo() {
mon.DisableLog()
}
// RedactJSON replaces known secret fields in a JSON blob with "[REDACTED]".
func RedactJSON(raw string) string {
if raw == "" {
return raw
}
return sensitiveKeyPattern.ReplaceAllString(raw, `${1}"[REDACTED]"`)
}
// RedactAny marshals v then redacts; for ad-hoc logx of structs.
func RedactAny(v any) string {
if v == nil {
return "null"
}
b, err := json.Marshal(v)
if err != nil {
return "[unserializable]"
}
return RedactJSON(string(b))
}
// MaskPassword returns a fixed placeholder (never the input).
func MaskPassword(_ string) string { return "[REDACTED]" }
// IsSensitiveKey reports if a field name must never be logged.
func IsSensitiveKey(name string) bool {
n := strings.ToLower(strings.TrimSpace(name))
switch n {
case "password", "new_password", "current_password", "temporary_password",
"password_hash", "access_token", "refresh_token", "api_key", "secret",
"authorization", "cookie", "content": // media content is base64 blob
return true
default:
return false
}
}

View File

@ -0,0 +1,29 @@
package securelog
import "testing"
func TestRedactJSON(t *testing.T) {
in := `{"email":"a@b.com","password":"Secret12!@#a","password_hash":"$2a$10$abc","new_password":"x","ok":true}`
out := RedactJSON(in)
if contains(out, "Secret12") || contains(out, "$2a$10$abc") || contains(out, `"x"`) {
t.Fatalf("still has secret: %s", out)
}
if !contains(out, "[REDACTED]") {
t.Fatalf("expected redacted: %s", out)
}
if !contains(out, "a@b.com") {
t.Fatalf("email should remain: %s", out)
}
}
func contains(s, sub string) bool {
return len(s) >= len(sub) && (s == sub || len(sub) == 0 ||
(len(s) > 0 && (func() bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
})()))
}

View File

@ -0,0 +1,70 @@
package admin
import (
"context"
"strconv"
"apps/backend/internal/module/member/domain"
memberUC "apps/backend/internal/module/member/usecase"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type AdminResetPasswordLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAdminResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminResetPasswordLogic {
return &AdminResetPasswordLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *AdminResetPasswordLogic) AdminResetPassword(req *types.AdminResetPasswordReq) (resp *types.AdminCreateMemberData, err error) {
uid, err := strconv.ParseInt(req.Uid, 10, 64)
if err != nil {
return nil, err
}
tmp := req.NewPassword
if tmp == "" {
tmp = req.Password
}
if tmp == "" {
tmp = memberUC.GenerateTempPassword()
} else if err := memberUC.ValidatePasswordPolicy(tmp); err != nil {
return nil, err
}
m, err := l.svcCtx.Members.FindByUID(l.ctx, uid)
if err != nil {
return nil, err
}
loginID := m.Email
if loginID == "" {
loginID = m.Phone
}
if loginID == "" {
return nil, domain.ErrNotFound
}
// prefer identity path; fall back to member hash + create identity
if err := l.svcCtx.Auth.UpdateUserToken(l.ctx, loginID, domain.PlatformPassword, tmp); err != nil {
hash, herr := memberUC.HashPassword(tmp, 10)
if herr != nil {
return nil, herr
}
m.PasswordHash = hash
m.UpdatedAt = domain.NowNano()
if err := l.svcCtx.Members.UpdateMember(l.ctx, m); err != nil {
return nil, err
}
_ = l.svcCtx.Members.CreateIdentity(l.ctx, &domain.Identity{
UID: uid, LoginID: loginID, Platform: domain.PlatformPassword,
AccountType: domain.AccountTypeEmail, PasswordHash: hash,
CreatedAt: domain.NowNano(), UpdatedAt: domain.NowNano(),
})
} else {
m, _ = l.svcCtx.Members.FindByUID(l.ctx, uid)
}
return &types.AdminCreateMemberData{User: *types.MemberFromModel(m), TemporaryPassword: tmp}, nil
}

View File

@ -0,0 +1,68 @@
package admin
import (
"context"
"strings"
"apps/backend/internal/module/member/domain"
memberUC "apps/backend/internal/module/member/usecase"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type CreateMemberLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCreateMemberLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateMemberLogic {
return &CreateMemberLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *CreateMemberLogic) CreateMember(req *types.AdminCreateMemberReq) (resp *types.AdminCreateMemberData, err error) {
tmp := req.Password
if tmp == "" {
tmp = memberUC.GenerateTempPassword()
} else if err := memberUC.ValidatePasswordPolicy(tmp); err != nil {
return nil, err
}
email := strings.ToLower(strings.TrimSpace(req.Email))
m, _, err := l.svcCtx.Auth.CreateUserAccount(l.ctx, domain.CreateLoginUserRequest{
LoginID: email, Platform: domain.PlatformPassword, AccountType: domain.AccountTypeEmail,
Password: tmp, DisplayName: req.DisplayName, Email: email, Phone: req.Phone,
})
if err != nil {
return nil, err
}
// admin-created: apply roles / verified / extra profile
now := domain.NowNano()
if len(req.Roles) > 0 {
m.Roles = req.Roles
}
if req.EmailVerified {
m.EmailVerified = true
m.EmailVerifiedAt = &now
m.Status = domain.StatusActive
}
if req.Bio != "" {
m.Bio = req.Bio
}
if req.Nickname != "" {
m.Nickname = req.Nickname
}
if req.FullName != "" {
m.FullName = req.FullName
}
m.UpdatedAt = now
if err := l.svcCtx.Members.UpdateMember(l.ctx, m); err != nil {
return nil, err
}
ids, _ := l.svcCtx.Auth.ListIdentities(l.ctx, m.UID)
return &types.AdminCreateMemberData{
User: *types.MemberFromModelWithIdentities(m, ids),
TemporaryPassword: tmp,
}, nil
}

View File

@ -0,0 +1,33 @@
package admin
import (
"context"
"strconv"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetMemberLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetMemberLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMemberLogic {
return &GetMemberLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *GetMemberLogic) GetMember(req *types.AdminUidPath) (resp *types.MemberPublic, err error) {
uid, err := strconv.ParseInt(req.Uid, 10, 64)
if err != nil {
return nil, err
}
m, err := l.svcCtx.Members.FindByUID(l.ctx, uid)
if err != nil {
return nil, err
}
return types.MemberFromModel(m), nil
}

View File

@ -0,0 +1,37 @@
package admin
import (
"context"
"strconv"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ListMemberIdentitiesLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewListMemberIdentitiesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListMemberIdentitiesLogic {
return &ListMemberIdentitiesLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *ListMemberIdentitiesLogic) ListMemberIdentities(req *types.AdminUidPath) (resp *types.AdminMemberIdentitiesData, err error) {
uid, err := strconv.ParseInt(req.Uid, 10, 64)
if err != nil {
return nil, err
}
ids, err := l.svcCtx.Auth.ListIdentities(l.ctx, uid)
if err != nil {
return nil, err
}
list := make([]types.IdentityPublic, 0, len(ids))
for _, id := range ids {
list = append(list, types.IdentityFromModel(id))
}
return &types.AdminMemberIdentitiesData{List: list}, nil
}

View File

@ -0,0 +1,49 @@
package admin
import (
"context"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ListMembersLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewListMembersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListMembersLogic {
return &ListMembersLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *ListMembersLogic) ListMembers(req *types.AdminListMembersReq) (resp *types.AdminListData, err error) {
page, pageSize := req.Page, req.PageSize
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
list, total, err := l.svcCtx.Members.ListPage(l.ctx, page, pageSize, req.Query, req.Status)
if err != nil {
return nil, err
}
items := make([]types.MemberPublic, 0, len(list))
for _, m := range list {
items = append(items, *types.MemberFromModel(m))
}
totalPages := int(total) / pageSize
if int(total)%pageSize != 0 {
totalPages++
}
if totalPages == 0 {
totalPages = 1
}
return &types.AdminListData{
List: items,
Pagination: types.Pagination{Page: page, PageSize: pageSize, Total: total, TotalPages: totalPages},
}, nil
}

View File

@ -0,0 +1,48 @@
package admin
import (
"context"
"strconv"
"apps/backend/internal/module/member/domain"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type SetEmailVerifiedLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewSetEmailVerifiedLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetEmailVerifiedLogic {
return &SetEmailVerifiedLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *SetEmailVerifiedLogic) SetEmailVerified(req *types.AdminEmailVerifiedReq) (resp *types.MemberPublic, err error) {
uid, err := strconv.ParseInt(req.Uid, 10, 64)
if err != nil {
return nil, err
}
m, err := l.svcCtx.Members.FindByUID(l.ctx, uid)
if err != nil {
return nil, err
}
m.EmailVerified = req.Verified
if req.Verified {
now := domain.NowNano()
m.EmailVerifiedAt = &now
if m.Status == domain.StatusUnverified {
m.Status = domain.StatusActive
}
} else {
m.EmailVerifiedAt = nil
}
m.UpdatedAt = domain.NowNano()
if err := l.svcCtx.Members.UpdateMember(l.ctx, m); err != nil {
return nil, err
}
return types.MemberFromModel(m), nil
}

View File

@ -0,0 +1,51 @@
package admin
import (
"context"
"strconv"
"apps/backend/internal/module/member/domain"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type SetRolesLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewSetRolesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetRolesLogic {
return &SetRolesLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *SetRolesLogic) SetRoles(req *types.AdminRolesReq) (resp *types.MemberPublic, err error) {
uid, err := strconv.ParseInt(req.Uid, 10, 64)
if err != nil {
return nil, err
}
m, err := l.svcCtx.Members.FindByUID(l.ctx, uid)
if err != nil {
return nil, err
}
wasAdmin, willAdmin := m.IsAdmin(), false
for _, r := range req.Roles {
if r == domain.RoleAdmin {
willAdmin = true
}
}
if wasAdmin && !willAdmin {
n, _ := l.svcCtx.Members.CountActiveAdmins(l.ctx)
if n <= 1 {
return nil, domain.ErrLastAdmin
}
}
m.Roles = req.Roles
m.UpdatedAt = domain.NowNano()
if err := l.svcCtx.Members.UpdateMember(l.ctx, m); err != nil {
return nil, err
}
return types.MemberFromModel(m), nil
}

View File

@ -0,0 +1,40 @@
package admin
import (
"context"
"strconv"
"apps/backend/internal/module/member/domain"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type SetStatusLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewSetStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetStatusLogic {
return &SetStatusLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *SetStatusLogic) SetStatus(req *types.AdminStatusReq) (resp *types.MemberPublic, err error) {
uid, err := strconv.ParseInt(req.Uid, 10, 64)
if err != nil {
return nil, err
}
status := req.Status
switch status {
case domain.StatusActive, domain.StatusSuspended, domain.StatusUnverified, domain.StatusUninitialized:
default:
return nil, domain.ErrNotFound // treat invalid as not found-ish; better weak password reuse? use ErrNotFound ok for now
}
m, err := l.svcCtx.Auth.UpdateStatus(l.ctx, uid, status)
if err != nil {
return nil, err
}
return types.MemberFromModel(m), nil
}

View File

@ -0,0 +1,53 @@
package admin
import (
"context"
"strconv"
"apps/backend/internal/middleware"
"apps/backend/internal/module/member/domain"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type SetSuspendedLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewSetSuspendedLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetSuspendedLogic {
return &SetSuspendedLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *SetSuspendedLogic) SetSuspended(req *types.AdminSuspendReq) (resp *types.MemberPublic, err error) {
actor, _ := middleware.UIDFrom(l.ctx)
uid, err := strconv.ParseInt(req.Uid, 10, 64)
if err != nil {
return nil, err
}
if req.Suspended && uid == actor {
return nil, domain.ErrSelfSuspend
}
m, err := l.svcCtx.Members.FindByUID(l.ctx, uid)
if err != nil {
return nil, err
}
if req.Suspended && m.IsAdmin() {
n, _ := l.svcCtx.Members.CountActiveAdmins(l.ctx)
if n <= 1 {
return nil, domain.ErrLastAdmin
}
}
status := domain.StatusActive
if req.Suspended {
status = domain.StatusSuspended
}
m, err = l.svcCtx.Auth.UpdateStatus(l.ctx, uid, status)
if err != nil {
return nil, err
}
return types.MemberFromModel(m), nil
}

View File

@ -0,0 +1,31 @@
package auth
import (
"context"
"apps/backend/internal/middleware"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type BindAccountLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewBindAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindAccountLogic {
return &BindAccountLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *BindAccountLogic) BindAccount(req *types.AuthBindReq) (resp *types.IdentityPublic, err error) {
uid, _ := middleware.UIDFrom(l.ctx)
id, err := l.svcCtx.Auth.BindAccount(l.ctx, uid, req.LoginId, req.Platform, req.AccountType, req.Password)
if err != nil {
return nil, err
}
out := types.IdentityFromModel(id)
return &out, nil
}

View File

@ -0,0 +1,34 @@
package auth
import (
"context"
"apps/backend/internal/middleware"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ListIdentitiesLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewListIdentitiesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListIdentitiesLogic {
return &ListIdentitiesLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *ListIdentitiesLogic) ListIdentities() (resp *types.AuthIdentitiesData, err error) {
uid, _ := middleware.UIDFrom(l.ctx)
ids, err := l.svcCtx.Auth.ListIdentities(l.ctx, uid)
if err != nil {
return nil, err
}
list := make([]types.IdentityPublic, 0, len(ids))
for _, id := range ids {
list = append(list, types.IdentityFromModel(id))
}
return &types.AuthIdentitiesData{List: list}, nil
}

View File

@ -0,0 +1,42 @@
package auth
import (
"context"
"strings"
"apps/backend/internal/module/member/domain"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type LoginLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
return &LoginLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *LoginLogic) Login(req *types.AuthLoginReq) (resp *types.AuthSessionData, err error) {
loginID := strings.TrimSpace(req.LoginId)
if loginID == "" {
loginID = strings.TrimSpace(req.Email)
}
platform := req.Platform
if platform == "" {
platform = domain.PlatformPassword
}
m, pair, err := l.svcCtx.Auth.LoginPassword(l.ctx, loginID, req.Password, platform)
if err != nil {
return nil, err
}
ids, _ := l.svcCtx.Auth.ListIdentities(l.ctx, m.UID)
return &types.AuthSessionData{
Tokens: types.TokenPairFromAuth(pair.AccessToken, pair.RefreshToken, pair.TokenType, pair.ExpiresIn),
Member: *types.MemberFromModelWithIdentities(m, ids),
}, nil
}

View File

@ -0,0 +1,29 @@
package auth
import (
"context"
"apps/backend/internal/middleware"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type LogoutAllLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewLogoutAllLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogoutAllLogic {
return &LogoutAllLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *LogoutAllLogic) LogoutAll() (resp *types.AuthLogoutAllData, err error) {
uid, _ := middleware.UIDFrom(l.ctx)
if err := l.svcCtx.Auth.LogoutAll(l.ctx, uid); err != nil {
return nil, err
}
return &types.AuthLogoutAllData{Ok: true}, nil
}

View File

@ -0,0 +1,25 @@
package auth
import (
"context"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type LogoutLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewLogoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LogoutLogic {
return &LogoutLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *LogoutLogic) Logout(req *types.AuthLogoutReq) (resp *types.OkData, err error) {
l.svcCtx.Auth.Logout(req.RefreshToken)
return &types.OkData{Ok: true, Message: "logged out"}, nil
}

View File

@ -0,0 +1,31 @@
package auth
import (
"context"
"apps/backend/internal/middleware"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type MeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewMeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MeLogic {
return &MeLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *MeLogic) Me() (resp *types.MemberPublic, err error) {
uid, _ := middleware.UIDFrom(l.ctx)
m, err := l.svcCtx.Auth.Me(l.ctx, uid)
if err != nil {
return nil, err
}
ids, _ := l.svcCtx.Auth.ListIdentities(l.ctx, m.UID)
return types.MemberFromModelWithIdentities(m, ids), nil
}

View File

@ -0,0 +1,94 @@
package auth
import (
"context"
"strings"
"apps/backend/internal/module/member/domain"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type OAuthCallbackLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewOAuthCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OAuthCallbackLogic {
return &OAuthCallbackLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *OAuthCallbackLogic) OAuthCallback(req *types.AuthOAuthCallbackReq) (resp *types.AuthSessionData, err error) {
provider := strings.ToLower(strings.TrimSpace(req.Provider))
if provider == "" {
provider = domain.PlatformGoogle
}
var subject, email, name string
switch provider {
case domain.PlatformGoogle:
token := req.AccessToken
if token == "" {
token = req.IdToken
}
if token == "" {
token = req.Code
}
// allow mock: code or token "mock:sub:email"
if token == "" {
return nil, domain.ErrOAuthFailed
}
if !strings.HasPrefix(token, "mock:") && req.Code != "" && strings.HasPrefix(req.Code, "mock:") {
token = req.Code
}
subject, email, name, err = l.svcCtx.Auth.VerifyGoogleIDToken(l.ctx, token)
if err != nil {
// dev fallback when Google not configured: treat code as mock subject
if req.Code != "" {
subject = provider + "_" + req.Code
email = subject + "@oauth.local"
name = provider + " user"
err = nil
} else {
return nil, err
}
}
case domain.PlatformLine:
code := req.Code
if code == "" {
code = req.AccessToken
}
subject, name, email, err = l.svcCtx.Auth.LineCodeToProfile(l.ctx, code, req.RedirectUri)
if err != nil {
if code != "" {
subject = provider + "_" + code
name = provider + " user"
err = nil
} else {
return nil, err
}
}
default:
// generic mock oauth
code := req.Code
if code == "" {
code = "anon"
}
subject = provider + "_" + code
email = subject + "@oauth.local"
name = provider + " user"
}
m, pair, err := l.svcCtx.Auth.LoginOAuth(l.ctx, provider, subject, email, name)
if err != nil {
return nil, err
}
ids, _ := l.svcCtx.Auth.ListIdentities(l.ctx, m.UID)
return &types.AuthSessionData{
Tokens: types.TokenPairFromAuth(pair.AccessToken, pair.RefreshToken, pair.TokenType, pair.ExpiresIn),
Member: *types.MemberFromModelWithIdentities(m, ids),
}, nil
}

View File

@ -0,0 +1,32 @@
package auth
import (
"context"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type OAuthStartLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewOAuthStartLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OAuthStartLogic {
return &OAuthStartLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *OAuthStartLogic) OAuthStart(req *types.AuthOAuthProviderPath) (resp *types.AuthOAuthStartData, err error) {
p := req.Provider
if p == "" {
p = "google"
}
return &types.AuthOAuthStartData{
AuthorizeUrl: "https://example.com/oauth/" + p + "/authorize?state=dev",
Provider: p,
State: "dev",
}, nil
}

View File

@ -0,0 +1,30 @@
package auth
import (
"context"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RefreshLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewRefreshLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshLogic {
return &RefreshLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *RefreshLogic) Refresh(req *types.AuthRefreshReq) (resp *types.AuthTokensOnly, err error) {
pair, err := l.svcCtx.Auth.Refresh(l.ctx, req.RefreshToken)
if err != nil {
return nil, err
}
return &types.AuthTokensOnly{
Tokens: types.TokenPairFromAuth(pair.AccessToken, pair.RefreshToken, pair.TokenType, pair.ExpiresIn),
}, nil
}

View File

@ -0,0 +1,36 @@
package auth
import (
"context"
"apps/backend/internal/module/member/domain"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RegisterLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
return &RegisterLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *RegisterLogic) Register(req *types.AuthRegisterReq) (resp *types.AuthSessionData, err error) {
m, pair, err := l.svcCtx.Auth.CreateUserAccount(l.ctx, domain.CreateLoginUserRequest{
LoginID: req.Email, Platform: domain.PlatformPassword, AccountType: domain.AccountTypeEmail,
Password: req.Password, DisplayName: req.DisplayName, Email: req.Email, Phone: req.Phone,
})
if err != nil {
return nil, err
}
ids, _ := l.svcCtx.Auth.ListIdentities(l.ctx, m.UID)
return &types.AuthSessionData{
Tokens: types.TokenPairFromAuth(pair.AccessToken, pair.RefreshToken, pair.TokenType, pair.ExpiresIn),
Member: *types.MemberFromModelWithIdentities(m, ids),
}, nil
}

View File

@ -0,0 +1,94 @@
package auth
import (
"context"
"net/url"
"strings"
notifDomain "apps/backend/internal/module/notification/domain"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RequestPasswordResetLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewRequestPasswordResetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RequestPasswordResetLogic {
return &RequestPasswordResetLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// RequestPasswordReset 產生重設碼 → 寄信。信箱不存在回錯誤、不寄。
func (l *RequestPasswordResetLogic) RequestPasswordReset(req *types.AuthRequestResetReq) (resp *types.AuthRequestResetData, err error) {
email := strings.ToLower(strings.TrimSpace(req.Email))
code, err := l.svcCtx.Auth.RequestPasswordReset(l.ctx, email)
if err != nil {
return nil, err
}
relPath := resetPath(email, code)
absURL := absoluteWebURL(l.svcCtx.Config.PublicWebBase, relPath)
name := strings.Split(email, "@")[0]
langRaw := req.Language
if m, findErr := l.svcCtx.Members.FindByEmail(l.ctx, email); findErr == nil && m != nil {
if m.DisplayName != "" {
name = m.DisplayName
}
if langRaw == "" && m.PreferredLanguage != "" {
langRaw = m.PreferredLanguage
}
}
lang := notifDomain.NormalizeLanguage(langRaw)
subject, html, err := l.svcCtx.Notification.RenderVerify(
notifDomain.TemplatePasswordReset,
lang,
notifDomain.VerifyTemplateVars{
Username: name,
VerifyCode: code,
ResetURL: absURL,
},
)
if err != nil {
l.Errorf("render password reset template: %v", err)
return nil, err
}
if err := l.svcCtx.Notification.SendEmail(l.ctx, notifDomain.EmailRequest{
RecipientEmail: email,
SenderEmail: l.svcCtx.Notification.SenderFrom(),
Subject: subject,
HTMLBody: html,
}); err != nil {
l.Errorf("send password reset to %s: %v", email, err)
return nil, err
}
return &types.AuthRequestResetData{
Ok: true,
Message: "reset code sent",
}, nil
}
func resetPath(email, code string) string {
q := url.Values{}
q.Set("email", email)
q.Set("token", code)
return "/reset-password?" + q.Encode()
}
func absoluteWebURL(base, path string) string {
base = strings.TrimRight(strings.TrimSpace(base), "/")
if base == "" {
return path
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return base + path
}

View File

@ -0,0 +1,31 @@
package auth
import (
"context"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ResetPasswordLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewResetPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetPasswordLogic {
return &ResetPasswordLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *ResetPasswordLogic) ResetPassword(req *types.AuthResetPasswordReq) (resp *types.OkData, err error) {
code := req.Code
if code == "" {
code = req.Token
}
if err := l.svcCtx.Auth.ResetPassword(l.ctx, req.Email, code, req.NewPassword); err != nil {
return nil, err
}
return &types.OkData{Ok: true, Message: "password updated"}, nil
}

View File

@ -0,0 +1,72 @@
package auth
import (
"context"
"strings"
"apps/backend/internal/middleware"
notifDomain "apps/backend/internal/module/notification/domain"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type SendEmailCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewSendEmailCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendEmailCodeLogic {
return &SendEmailCodeLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// SendEmailCode 產生驗證碼 → hermes 多語模板 → 寄信。不回傳驗證碼給前端。
func (l *SendEmailCodeLogic) SendEmailCode() (resp *types.AuthSendCodeData, err error) {
uid, _ := middleware.UIDFrom(l.ctx)
code, err := l.svcCtx.Auth.SendEmailCode(l.ctx, uid)
if err != nil {
return nil, err
}
m, err := l.svcCtx.Members.FindByUID(l.ctx, uid)
if err != nil {
return nil, err
}
to := strings.TrimSpace(m.Email)
if to == "" {
return &types.AuthSendCodeData{Ok: true, Message: "verification code issued"}, nil
}
name := m.DisplayName
if name == "" {
name = strings.Split(to, "@")[0]
}
lang := notifDomain.NormalizeLanguage(m.PreferredLanguage)
subject, html, err := l.svcCtx.Notification.RenderVerify(
notifDomain.TemplateEmailVerify,
lang,
notifDomain.VerifyTemplateVars{Username: name, VerifyCode: code},
)
if err != nil {
l.Errorf("render email verify template: %v", err)
return nil, err
}
if err := l.svcCtx.Notification.SendEmail(l.ctx, notifDomain.EmailRequest{
RecipientEmail: to,
SenderEmail: l.svcCtx.Notification.SenderFrom(),
Subject: subject,
HTMLBody: html,
}); err != nil {
l.Errorf("send email verify to %s: %v", to, err)
return nil, err
}
return &types.AuthSendCodeData{
Ok: true,
Message: "verification code sent",
}, nil
}

View File

@ -0,0 +1,29 @@
package auth
import (
"context"
"apps/backend/internal/middleware"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type UnbindAccountLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUnbindAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnbindAccountLogic {
return &UnbindAccountLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *UnbindAccountLogic) UnbindAccount(req *types.AuthUnbindReq) (resp *types.OkData, err error) {
uid, _ := middleware.UIDFrom(l.ctx)
if err := l.svcCtx.Auth.UnbindAccount(l.ctx, uid, req.LoginId, req.Platform); err != nil {
return nil, err
}
return &types.OkData{Ok: true, Message: "unbound"}, nil
}

View File

@ -0,0 +1,47 @@
package auth
import (
"context"
"strings"
"apps/backend/internal/middleware"
"apps/backend/internal/response"
"apps/backend/internal/svc"
"apps/backend/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type UpdateProfileLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUpdateProfileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateProfileLogic {
return &UpdateProfileLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *UpdateProfileLogic) UpdateProfile(req *types.AuthProfilePatchReq) (resp *types.MemberPublic, err error) {
// 頭像nil=不改;""=清除;非空須為 media upload 回傳的 http(s) URL禁止 data URL
if req.AvatarUrl != nil {
u := strings.TrimSpace(*req.AvatarUrl)
if u != "" {
if strings.HasPrefix(u, "data:") || len(u) > 2048 {
return nil, response.Biz(400, 400001, "avatar_url must be a short http(s) URL from media upload, not data URL")
}
if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
return nil, response.Biz(400, 400001, "avatar_url must be http(s)")
}
}
// 正規化空白
*req.AvatarUrl = u
}
uid, _ := middleware.UIDFrom(l.ctx)
m, err := l.svcCtx.Auth.UpdateUserInfo(l.ctx, uid, types.PatchFromAuthProfile(req))
if err != nil {
return nil, err
}
ids, _ := l.svcCtx.Auth.ListIdentities(l.ctx, m.UID)
return types.MemberFromModelWithIdentities(m, ids), nil
}

Some files were not shown because too many files have changed in this diff Show More