From da51713159a095a1856751e2d62ff4a1ce135ced Mon Sep 17 00:00:00 2001 From: mac Date: Mon, 13 Jul 2026 14:39:56 +0800 Subject: [PATCH 01/13] Add authserver module --- authserver/.dockerignore | 9 + authserver/.env.example | 16 + authserver/.gitignore | 12 + authserver/Dockerfile.backend | 26 ++ authserver/README.md | 339 ++++++++++++++++++ authserver/docker-compose.mysql.yml | 19 ++ authserver/go.mod | 48 +++ authserver/go.sum | 102 ++++++ authserver/internal/auth/jwt.go | 114 +++++++ authserver/internal/auth/random.go | 14 + authserver/internal/config/config.go | 143 ++++++++ authserver/internal/database/database.go | 21 ++ authserver/internal/database/seed.go | 11 + authserver/internal/handler/context.go | 39 +++ authserver/internal/handler/health.go | 33 ++ authserver/internal/handler/oauth.go | 290 ++++++++++++++++ authserver/internal/handler/saml.go | 114 +++++++ authserver/internal/handler/user.go | 27 ++ authserver/internal/handler/wayen.go | 206 +++++++++++ authserver/internal/model/models.go | 68 ++++ authserver/internal/router/router.go | 56 +++ authserver/internal/service/audit.go | 60 ++++ authserver/internal/service/auth.go | 245 +++++++++++++ authserver/internal/service/wayen.go | 143 ++++++++ authserver/internal/sso/login.go | 415 +++++++++++++++++++++++ authserver/internal/sso/login_test.go | 190 +++++++++++ authserver/internal/sso/metadata.go | 147 ++++++++ authserver/web/.dockerignore | 4 + authserver/web/Dockerfile | 16 + authserver/web/index.html | 12 + authserver/web/nginx.conf | 53 +++ authserver/web/package-lock.json | 379 +++++++++++++++++++++ authserver/web/package.json | 20 ++ authserver/web/src/App.vue | 211 ++++++++++++ authserver/web/src/main.js | 5 + authserver/web/src/style.css | 230 +++++++++++++ authserver/web/vite.config.js | 15 + 37 files changed, 3852 insertions(+) create mode 100644 authserver/.dockerignore create mode 100644 authserver/.env.example create mode 100644 authserver/.gitignore create mode 100644 authserver/Dockerfile.backend create mode 100644 authserver/README.md create mode 100644 authserver/docker-compose.mysql.yml create mode 100644 authserver/go.mod create mode 100644 authserver/go.sum create mode 100644 authserver/internal/auth/jwt.go create mode 100644 authserver/internal/auth/random.go create mode 100644 authserver/internal/config/config.go create mode 100644 authserver/internal/database/database.go create mode 100644 authserver/internal/database/seed.go create mode 100644 authserver/internal/handler/context.go create mode 100644 authserver/internal/handler/health.go create mode 100644 authserver/internal/handler/oauth.go create mode 100644 authserver/internal/handler/saml.go create mode 100644 authserver/internal/handler/user.go create mode 100644 authserver/internal/handler/wayen.go create mode 100644 authserver/internal/model/models.go create mode 100644 authserver/internal/router/router.go create mode 100644 authserver/internal/service/audit.go create mode 100644 authserver/internal/service/auth.go create mode 100644 authserver/internal/service/wayen.go create mode 100644 authserver/internal/sso/login.go create mode 100644 authserver/internal/sso/login_test.go create mode 100644 authserver/internal/sso/metadata.go create mode 100644 authserver/web/.dockerignore create mode 100644 authserver/web/Dockerfile create mode 100644 authserver/web/index.html create mode 100644 authserver/web/nginx.conf create mode 100644 authserver/web/package-lock.json create mode 100644 authserver/web/package.json create mode 100644 authserver/web/src/App.vue create mode 100644 authserver/web/src/main.js create mode 100644 authserver/web/src/style.css create mode 100644 authserver/web/vite.config.js diff --git a/authserver/.dockerignore b/authserver/.dockerignore new file mode 100644 index 0000000..6383146 --- /dev/null +++ b/authserver/.dockerignore @@ -0,0 +1,9 @@ +.cache/ +.git/ +.DS_Store +.env +bin/ +data/ +web/node_modules/ +web/dist/ + diff --git a/authserver/.env.example b/authserver/.env.example new file mode 100644 index 0000000..827f866 --- /dev/null +++ b/authserver/.env.example @@ -0,0 +1,16 @@ +APP_ENV=dev +HTTP_ADDR=:8080 +PUBLIC_BASE_URL=http://localhost:8080 + +MYSQL_DSN=auth:auth@tcp(127.0.0.1:3000)/authserver?charset=utf8mb4&parseTime=True&loc=Local +AUTO_MIGRATE=true + +JWT_SECRET=change-this-secret +JWT_ISSUER=authserver +JWT_TTL_MINUTES=120 + +SAML_ENTITY_ID=http://localhost:8080/api/v1/saml/metadata +SAML_ACS_URL=http://localhost:8080/api/v1/saml/acs +SAML_IDP_METADATA_URL=http://sso-internal.dev.qiniu.io/saml2/meta +SAML_SP_CERT_FILE=certs/sp.crt +SAML_SP_KEY_FILE=certs/sp.key diff --git a/authserver/.gitignore b/authserver/.gitignore new file mode 100644 index 0000000..24fa82a --- /dev/null +++ b/authserver/.gitignore @@ -0,0 +1,12 @@ +.cache/ +.env +.DS_Store +*.tar +bin/ +server +web/node_modules/ +web/dist/ +certs/ +scripts/ +data/ +docs/ diff --git a/authserver/Dockerfile.backend b/authserver/Dockerfile.backend new file mode 100644 index 0000000..527ec8e --- /dev/null +++ b/authserver/Dockerfile.backend @@ -0,0 +1,26 @@ +FROM golang:1.26-alpine AS builder + +WORKDIR /app + +ENV GOPROXY=https://goproxy.cn,direct \ + GOSUMDB=sum.golang.google.cn + +COPY go.mod go.sum ./ +RUN go mod download + +COPY cmd ./cmd +COPY internal ./internal + +RUN CGO_ENABLED=0 GOOS=linux go build -o /out/authserver ./cmd/server + +FROM alpine:3.22 + +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /authserver/backend + +COPY --from=builder /out/authserver /usr/local/bin/authserver + +EXPOSE 8083 + +CMD ["authserver"] diff --git a/authserver/README.md b/authserver/README.md new file mode 100644 index 0000000..5cba5e2 --- /dev/null +++ b/authserver/README.md @@ -0,0 +1,339 @@ +# AuthServer + +最小 Gin + MySQL 权限服务实践。 + +## 当前能力 + +- Gin HTTP 服务 +- MySQL / GORM 连接 +- 核心表 AutoMigrate +- 默认环境和身份源初始化 +- 健康检查 +- 本地登录接口骨架 +- LDAP / 内部 SSO provider 预留 +- 权限判断接口骨架 +- 管理员用户、用户组、角色、权限、授权绑定 API +- 业务线、namespace、cluster 管理 API +- Rancher 同步预览接口 +- 审计日志落库 +- Vue 管理后台 + +## 启动 + +```bash +cp .env.example .env +go mod tidy +go run ./cmd/server +``` + +本地 Docker MySQL 8.0.46: + +```bash +docker compose -f docker-compose.mysql.yml up -d +``` + +这个 MySQL 会映射到本机 `3000` 端口,`.env.example` 默认 DSN 已按这个端口配置: + +```env +MYSQL_DSN=auth:auth@tcp(127.0.0.1:3000)/authserver?charset=utf8mb4&parseTime=True&loc=Local +``` + +连接测试: + +```bash +mysql -h127.0.0.1 -P3000 -uauth -pauth authserver +``` + +前端开发服务: + +```bash +cd web +npm install +npm run dev +``` + +默认访问: + +```text +http://127.0.0.1:5173 +``` + +Vite 会把 `/api` 代理到后端 `http://127.0.0.1:8080`。 + +如果 MySQL 还没有创建库和账号,可以用 root 执行: + +```bash +mysql -uroot -p < docs/mysql-init.sql +``` + +如果你使用的是 Docker Desktop、OrbStack、Colima 或容器里的 MySQL,应用访问 MySQL 时来源 IP 可能不是 `localhost`,而是类似 `192.168.65.1`。这时 MySQL 账号需要允许远程来源,`docs/mysql-init.sql` 已经包含 `'auth'@'%'`。 + +如果要自动创建本地管理员账号,在 `.env` 里设置: + +```bash +BOOTSTRAP_ADMIN_USERNAME=admin +BOOTSTRAP_ADMIN_PASSWORD='your-password' +``` + +也可以直接使用环境变量: + +```bash +MYSQL_DSN='auth:auth@tcp(127.0.0.1:3306)/authserver?charset=utf8mb4&parseTime=True&loc=Local' \ +JWT_SECRET='change-this-secret' \ +BOOTSTRAP_ADMIN_PASSWORD='your-password' \ +go run ./cmd/server +``` + +## 发版 + +发布脚本会在本地完成: + +- 构建后端和前端 Docker 镜像 +- 读取本地 `.env`,生成并应用线上 `ConfigMap`/`Secret` +- 导出 `authserver-images-.tar` +- 上传到服务器 +- 导入 RKE2 使用的 `containerd` +- 更新 `authserver` namespace 下的 deployment 并等待 rollout 完成 + +默认目标与你当前线上环境一致: + +- 主机:`root@218.11.5.223` +- 私钥:`/Users/mac/dev/xengineer-cs2.pem` +- 远端目录:`/authserver` +- containerd socket:`/run/k3s/containerd/containerd.sock` + +发布脚本默认把仓库根目录的 `.env` 当作线上配置源。也就是说,发版前需要先把 `.env` 改成目标环境的实际值;脚本会在发版时重新生成并应用 `authserver-config` 和 `authserver-secret`。 + +脚本默认不会拦截测试环境配置。如果你要对正式环境启用更严格的保护,可以显式打开: + +```bash +STRICT_DEPLOY_ENV=true bash scripts/release.sh 1.0.5 +``` + +开启后会拦截几类明显错误: + +- `APP_ENV != prod` +- `MYSQL_DSN` 指向 `127.0.0.1` 或 `localhost` +- `PUBLIC_BASE_URL`、`SAML_ENTITY_ID`、`SAML_ACS_URL`、`WAYEN_LOGIN_URL`、`WAYEN_TARGET_URL` 指向 `127.0.0.1` 或 `localhost` + +执行方式: + +```bash +bash scripts/release.sh 1.0.5 +``` + +如果需要覆盖默认值,可以传环境变量: + +```bash +ENV_FILE=.env \ +DEPLOY_HOST=218.11.5.223 \ +DEPLOY_KEY=/path/to/key.pem \ +REMOTE_DIR=/authserver \ +bash scripts/release.sh 1.0.5 +``` + +如果只是想在服务器上重启已部署的服务,可以执行: + +```bash +bash scripts/restart-authserver.sh +``` + +也支持只重启单个 deployment: + +```bash +bash scripts/restart-authserver.sh backend +bash scripts/restart-authserver.sh nginx +``` + +## 接口 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/healthz` | 服务健康检查 | +| `GET` | `/readyz` | 数据库连接检查 | +| `POST` | `/api/v1/login` | 本地登录 | +| `POST` | `/api/v1/login/ldap` | LDAP 登录预留 | +| `GET` | `/api/v1/login/:provider` | SSO 跳转预留 | +| `GET` | `/api/v1/login/:provider/callback` | SSO 回调预留 | +| `POST` | `/api/v1/authz/check` | 权限判断 | +| `GET` | `/api/v1/users/me` | 当前用户 | +| `GET` | `/api/v1/wayen/login` | 根据 token 邮箱登录 Wayen 并跳转 | +| `GET` | `/api/v1/saml/metadata` | SAML SP metadata | +| `GET` | `/api/v1/login/internal-sso` | 发起 SAML SSO 登录 | +| `POST` | `/api/v1/saml/acs` | SAML ACS 回调,当前仅调试打印 | +| `GET` | `/auth/oauth/authorize` | OAuth2 Authorization Code 授权入口,供 Wayne 使用 | +| `POST` | `/auth/oauth/token` | OAuth2 code 换 access token | +| `GET` | `/auth/oauth/userinfo` | OAuth2 bearer token 查询当前用户 | + +## SAML Metadata + +服务会根据 `.env` 和 `certs/sp.crt` 动态生成 SP metadata,不需要手工维护 `certs/spmeta`。 +发起 SSO 登录时会实时请求 `.env` 里的 `SAML_IDP_METADATA_URL`,不再读取本地 `certs/idpmeta`。 + +本地后端如果运行在 `8083`,建议配置: + +```env +HTTP_ADDR=:8083 +PUBLIC_BASE_URL=http://localhost:8083 +SAML_ENTITY_ID=http://localhost:8083/api/v1/saml/metadata +SAML_ACS_URL=http://localhost:8083/api/v1/saml/acs +SAML_IDP_METADATA_URL=http://sso-internal.dev.qiniu.io/saml2/meta +SAML_SP_CERT_FILE=certs/sp.crt +SAML_SP_KEY_FILE=certs/sp.key +``` + +给 IdP 配置的 SP metadata 地址: + +```text +http://localhost:8083/api/v1/saml/metadata +``` + +调试 SSO 登录入口: + +```text +http://localhost:8083/api/v1/login/internal-sso +``` + +ACS 会处理 IdP 回调: + +```text +POST /api/v1/saml/acs +``` + +收到 IdP 回调后,会把 SAMLResponse 解码;如果 Assertion 被加密,会用 `SAML_SP_KEY_FILE` 对应私钥解密,并在后端控制台打印解密后的摘要和 Assertion: + +- Response ID +- InResponseTo +- Issuer +- Destination +- NameID +- Attributes +- 解密后的 Assertion XML + +当前 SAML 用户落库规则: + +- `eduPersonPrincipalName` 同时作为 `users.username` 和 `users.email`。 +- `NameID` 写入 `users.external_id`。 +- 首次登录会自动创建 `source=saml` 的用户。 +- 不再创建 `identity_providers` / `user_identities` 表记录。 +- 登录成功后签发 JWT,写入 `authserver_token` HttpOnly cookie,并带着 token 跳回前端。 + +当前阶段还没有做: + +- SAML 签名验证 + +## Wayne OAuth2 对接 + +方案 1 下 AuthServer 作为 OAuth2 Provider,Wayne 作为 OAuth2 Client。Wayne 登录时不再校验 AuthServer 里的 Wayne 密码,也不需要 AuthServer 存储 Wayne 资源组信息;AuthServer 只负责 SAML 登录、签发 token、返回 `name/email/display`,Wayne 收到 userinfo 后会在 Wayne 自己数据库里创建或更新本地用户,Wayne 的资源组和权限仍由 Wayne 自己维护。 + +AuthServer 端配置: + +```env +OAUTH_WAYNE_CLIENT_ID=wayne +OAUTH_WAYNE_CLIENT_SECRET=change-this-wayne-client-secret +OAUTH_WAYNE_REDIRECT_URI=http://127.0.0.1:8080/login/oauth2/oauth2 +OAUTH_CODE_TTL_SECONDS=120 +``` + +AuthServer 暴露给 Wayne 的 OAuth2 地址: + +```text +GET /auth/oauth/authorize +POST /auth/oauth/token +GET /auth/oauth/userinfo +``` + +Wayne `app.conf` 示例: + +```ini +[auth.oauth2] +enabled = true +redirect_url = http://127.0.0.1:8080 +client_id = wayne +client_secret = change-this-wayne-client-secret +auth_url = http://218.11.5.223/auth/oauth/authorize +token_url = http://218.11.5.223/auth/oauth/token +api_url = http://218.11.5.223/auth/oauth/userinfo +api_mapping = name:name,email:email,display:display +scopes = profile,email +``` + +Wayne 会把回调地址拼成: + +```text +{redirect_url}/login/oauth2/oauth2 +``` + +因此 `OAUTH_WAYNE_REDIRECT_URI` 必须和 Wayne 实际回调地址完全一致。浏览器访问 Wayne OAuth 登录入口后,如果 AuthServer 还没有登录态,会先跳内部 SAML;SAML 成功后再回到 OAuth authorize,签发 code 给 Wayne。 + +管理员可查看当前 SAML metadata 配置: + +```text +GET /api/v1/admin/saml/metadata/config +``` + +## 管理员接口 + +管理员接口需要 `Authorization: Bearer `,并且 token 里的用户必须是 `is_admin=true`。 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/api/v1/admin/users` | 用户列表 | +| `POST` | `/api/v1/admin/users` | 创建用户,可带本地密码 | +| `PUT` | `/api/v1/admin/users/:id` | 更新用户 | +| `DELETE` | `/api/v1/admin/users/:id` | 删除用户 | +| `GET` | `/api/v1/admin/groups` | 用户组列表 | +| `POST` | `/api/v1/admin/groups` | 创建用户组 | +| `POST` | `/api/v1/admin/groups/:id/members` | 用户加入组 | +| `DELETE` | `/api/v1/admin/groups/:id/members/:userID` | 用户移出组 | +| `GET` | `/api/v1/admin/roles` | 角色列表 | +| `POST` | `/api/v1/admin/roles` | 创建角色 | +| `PUT` | `/api/v1/admin/roles/:id/permissions` | 设置角色权限 | +| `GET` | `/api/v1/admin/permissions` | 权限点列表 | +| `POST` | `/api/v1/admin/permissions` | 创建权限点 | +| `GET` | `/api/v1/admin/business-lines` | 业务线列表 | +| `POST` | `/api/v1/admin/business-lines` | 创建业务线 | +| `GET` | `/api/v1/admin/namespaces` | namespace 列表 | +| `POST` | `/api/v1/admin/namespaces` | 创建 namespace | +| `GET` | `/api/v1/admin/environments` | 环境列表 | +| `GET` | `/api/v1/admin/clusters` | 集群列表 | +| `POST` | `/api/v1/admin/clusters` | 创建集群 | +| `GET` | `/api/v1/admin/wayen/credentials` | Wayen 凭据列表,不返回密码 | +| `POST` | `/api/v1/admin/wayen/credentials` | 按邮箱保存 Wayen 密码 | +| `DELETE` | `/api/v1/admin/wayen/credentials/:id` | 删除 Wayen 凭据 | +| `GET` | `/api/v1/admin/role-bindings` | 授权绑定列表 | +| `POST` | `/api/v1/admin/role-bindings` | 创建授权绑定 | +| `DELETE` | `/api/v1/admin/role-bindings/:id` | 删除授权绑定 | +| `POST` | `/api/v1/admin/rancher/sync` | Rancher 同步预览 | +| `GET` | `/api/v1/admin/rancher/sync-status` | Rancher 同步状态 | +| `GET` | `/api/v1/admin/saml/metadata/config` | SAML metadata 配置 | + +## Rancher 对齐流程 + +当前 Rancher 同步接口是 dry-run,不会真实调用 Rancher API。推荐先按下面流程把本系统数据配齐: + +1. 创建环境和集群:`rke2-dev`、`rke2-test`、`rke2-prod`。 +2. 创建业务线:例如 `pay`。 +3. 创建 namespace:例如 `pay`,归属支付业务线。 +4. 创建权限点:例如 `deployment:deploy`、`deployment:rollback`。 +5. 创建角色:例如 `prod-deployer`。 +6. 给角色绑定权限点。 +7. 创建用户组:例如 `pay-prod-deployer`。 +8. 把 SSO/LDAP 用户加入用户组。 +9. 创建授权绑定:`group + role + cluster + namespace`。 +10. 调用 `/api/v1/admin/rancher/sync` 查看将要同步到 Rancher 的映射。 + +核心授权绑定应该长这样: + +```json +{ + "subject_type": "group", + "subject_id": 1, + "role_id": 1, + "scope_type": "cluster_namespace", + "cluster_id": 3, + "namespace_id": 1 +} +``` + +这表示某个用户组在 prod 集群的 `pay` namespace 下拥有指定角色。 diff --git a/authserver/docker-compose.mysql.yml b/authserver/docker-compose.mysql.yml new file mode 100644 index 0000000..ff14f67 --- /dev/null +++ b/authserver/docker-compose.mysql.yml @@ -0,0 +1,19 @@ +services: + mysql: + image: mysql:8.0.46 + container_name: authserver-mysql + restart: unless-stopped + ports: + - "3000:3306" + environment: + MYSQL_ROOT_PASSWORD: root123456 + MYSQL_DATABASE: authserver + MYSQL_USER: auth + MYSQL_PASSWORD: auth + TZ: Asia/Shanghai + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --default-authentication-plugin=mysql_native_password + volumes: + - ./data/mysql:/var/lib/mysql diff --git a/authserver/go.mod b/authserver/go.mod new file mode 100644 index 0000000..16af137 --- /dev/null +++ b/authserver/go.mod @@ -0,0 +1,48 @@ +module authserver + +go 1.26 + +require ( + github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.0 + golang.org/x/crypto v0.40.0 + gorm.io/driver/mysql v1.6.0 + gorm.io/gorm v1.30.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/authserver/go.sum b/authserver/go.sum new file mode 100644 index 0000000..d4f772e --- /dev/null +++ b/authserver/go.sum @@ -0,0 +1,102 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= +gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/authserver/internal/auth/jwt.go b/authserver/internal/auth/jwt.go new file mode 100644 index 0000000..ef9e786 --- /dev/null +++ b/authserver/internal/auth/jwt.go @@ -0,0 +1,114 @@ +package auth + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type Claims struct { + UserID uint64 `json:"uid"` + Username string `json:"username"` + Email string `json:"email"` + IsAdmin bool `json:"admin"` + jwt.RegisteredClaims +} + +type OAuthCodeClaims struct { + UserID uint64 `json:"uid"` + ClientID string `json:"client_id"` + RedirectURI string `json:"redirect_uri"` + jwt.RegisteredClaims +} + +func Sign(secret, issuer string, ttl time.Duration, userID uint64, username, email string, isAdmin bool) (string, string, time.Time, error) { + now := time.Now() + expiresAt := now.Add(ttl) + tokenID := randomID() + claims := Claims{ + UserID: userID, + Username: username, + Email: email, + IsAdmin: isAdmin, + RegisteredClaims: jwt.RegisteredClaims{ + ID: tokenID, + Issuer: issuer, + Subject: username, + Audience: []string{"authserver"}, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(expiresAt), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + signed, err := token.SignedString([]byte(secret)) + return signed, tokenID, expiresAt, err +} + +func SignOAuthCode(secret, issuer string, ttl time.Duration, userID uint64, clientID, redirectURI string) (string, string, time.Time, error) { + now := time.Now() + expiresAt := now.Add(ttl) + codeID := randomID() + claims := OAuthCodeClaims{ + UserID: userID, + ClientID: clientID, + RedirectURI: redirectURI, + RegisteredClaims: jwt.RegisteredClaims{ + ID: codeID, + Issuer: issuer, + Subject: clientID, + Audience: []string{"oauth-code"}, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(expiresAt), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + signed, err := token.SignedString([]byte(secret)) + return signed, codeID, expiresAt, err +} + +func Parse(secret string, tokenValue string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenValue, &Claims{}, func(token *jwt.Token) (any, error) { + if token.Method != jwt.SigningMethodHS256 { + return nil, errors.New("unexpected jwt signing method") + } + return []byte(secret), nil + }) + if err != nil { + return nil, err + } + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return nil, errors.New("invalid jwt claims") + } + return claims, nil +} + +func ParseOAuthCode(secret string, tokenValue string) (*OAuthCodeClaims, error) { + token, err := jwt.ParseWithClaims(tokenValue, &OAuthCodeClaims{}, func(token *jwt.Token) (any, error) { + if token.Method != jwt.SigningMethodHS256 { + return nil, errors.New("unexpected jwt signing method") + } + return []byte(secret), nil + }) + if err != nil { + return nil, err + } + claims, ok := token.Claims.(*OAuthCodeClaims) + if !ok || !token.Valid { + return nil, errors.New("invalid oauth code claims") + } + if !claimStringsContains(claims.Audience, "oauth-code") { + return nil, errors.New("invalid oauth code audience") + } + return claims, nil +} + +func claimStringsContains(values jwt.ClaimStrings, target string) bool { + for _, value := range values { + if value == target { + return true + } + } + return false +} diff --git a/authserver/internal/auth/random.go b/authserver/internal/auth/random.go new file mode 100644 index 0000000..caeaa8d --- /dev/null +++ b/authserver/internal/auth/random.go @@ -0,0 +1,14 @@ +package auth + +import ( + "crypto/rand" + "encoding/hex" +) + +func randomID() string { + var b [16]byte + if _, err := rand.Read(b[:]); err != nil { + return "" + } + return hex.EncodeToString(b[:]) +} diff --git a/authserver/internal/config/config.go b/authserver/internal/config/config.go new file mode 100644 index 0000000..d5a63fa --- /dev/null +++ b/authserver/internal/config/config.go @@ -0,0 +1,143 @@ +package config + +import ( + "os" + "strconv" + "strings" + "time" +) + +type Config struct { + AppEnv string + HTTPAddr string + PublicBaseURL string + MySQLDSN string + AutoMigrate bool + JWTSecret string + JWTIssuer string + JWTTTLMinutes int + SAMLEntityID string + SAMLACSURL string + SAMLSPCert string + SAMLSPKey string + SAMLIDPMetaURL string + WayenLoginURL string + WayenTargetURL string + WayenUsernameKey string + WayenPasswordKey string + WayenLoginFormat string + WayenLoginValue string + OAuthClientID string + OAuthClientSecret string + OAuthRedirectURI string + OAuthCodeTTLSeconds int +} + +func Load() Config { + loadDotEnv(".env") + httpAddr := env("HTTP_ADDR", ":8080") + publicBaseURL := env("PUBLIC_BASE_URL", defaultPublicBaseURL(httpAddr)) + samlEntityID := env("SAML_ENTITY_ID", strings.TrimRight(publicBaseURL, "/")+"/auth/api/v1/saml/metadata") + samlACSURL := env("SAML_ACS_URL", strings.TrimRight(publicBaseURL, "/")+"/auth/api/v1/saml/acs") + + return Config{ + AppEnv: env("APP_ENV", "dev"), + HTTPAddr: httpAddr, + PublicBaseURL: publicBaseURL, + MySQLDSN: env("MYSQL_DSN", "auth:auth@tcp(127.0.0.1:3306)/authserver?charset=utf8mb4&parseTime=True&loc=Local"), + AutoMigrate: envBool("AUTO_MIGRATE", true), + JWTSecret: env("JWT_SECRET", "change-this-secret"), + JWTIssuer: env("JWT_ISSUER", "authserver"), + JWTTTLMinutes: envInt("JWT_TTL_MINUTES", 120), + SAMLEntityID: samlEntityID, + SAMLACSURL: samlACSURL, + SAMLSPCert: env("SAML_SP_CERT_FILE", "certs/sp.crt"), + SAMLSPKey: env("SAML_SP_KEY_FILE", "certs/sp.key"), + SAMLIDPMetaURL: env("SAML_IDP_METADATA_URL", "http://sso-internal.dev.qiniu.io/saml2/meta"), + WayenLoginURL: env("WAYEN_LOGIN_URL", ""), + WayenTargetURL: env("WAYEN_TARGET_URL", ""), + WayenUsernameKey: env("WAYEN_USERNAME_KEY", "email"), + WayenPasswordKey: env("WAYEN_PASSWORD_KEY", "password"), + WayenLoginFormat: env("WAYEN_LOGIN_FORMAT", "form"), + WayenLoginValue: env("WAYEN_LOGIN_VALUE", "email"), + OAuthClientID: env("OAUTH_WAYNE_CLIENT_ID", "wayne"), + OAuthClientSecret: env("OAUTH_WAYNE_CLIENT_SECRET", "wayne-secret"), + OAuthRedirectURI: env("OAUTH_WAYNE_REDIRECT_URI", ""), + OAuthCodeTTLSeconds: envInt("OAUTH_CODE_TTL_SECONDS", 120), + } +} + +func loadDotEnv(path string) { + content, err := os.ReadFile(path) + if err != nil { + return + } + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + key, value, ok := strings.Cut(line, "=") + if !ok { + continue + } + key = strings.TrimSpace(key) + value = strings.Trim(strings.TrimSpace(value), `"'`) + if key == "" || os.Getenv(key) != "" { + continue + } + _ = os.Setenv(key, value) + } +} + +func (c Config) JWTTTL() time.Duration { + return time.Duration(c.JWTTTLMinutes) * time.Minute +} + +func (c Config) OAuthCodeTTL() time.Duration { + return time.Duration(c.OAuthCodeTTLSeconds) * time.Second +} + +func env(key, fallback string) string { + value := os.Getenv(key) + if value == "" { + return fallback + } + return value +} + +func envBool(key string, fallback bool) bool { + value := os.Getenv(key) + if value == "" { + return fallback + } + parsed, err := strconv.ParseBool(value) + if err != nil { + return fallback + } + return parsed +} + +func envInt(key string, fallback int) int { + value := os.Getenv(key) + if value == "" { + return fallback + } + parsed, err := strconv.Atoi(value) + if err != nil { + return fallback + } + return parsed +} + +func defaultPublicBaseURL(httpAddr string) string { + addr := strings.TrimSpace(httpAddr) + if addr == "" { + return "http://localhost:8080" + } + if strings.HasPrefix(addr, ":") { + return "http://localhost" + addr + } + return "http://" + addr +} diff --git a/authserver/internal/database/database.go b/authserver/internal/database/database.go new file mode 100644 index 0000000..ca19b6e --- /dev/null +++ b/authserver/internal/database/database.go @@ -0,0 +1,21 @@ +package database + +import ( + "authserver/internal/model" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func Open(dsn string) (*gorm.DB, error) { + return gorm.Open(mysql.Open(dsn), &gorm.Config{}) +} + +func AutoMigrate(db *gorm.DB) error { + return db.AutoMigrate( + &model.User{}, + &model.WayenCredential{}, + &model.AccessToken{}, + &model.AuditLog{}, + ) +} diff --git a/authserver/internal/database/seed.go b/authserver/internal/database/seed.go new file mode 100644 index 0000000..2d06763 --- /dev/null +++ b/authserver/internal/database/seed.go @@ -0,0 +1,11 @@ +package database + +import ( + "authserver/internal/config" + + "gorm.io/gorm" +) + +func SeedDefaults(_ *gorm.DB, _ config.Config) error { + return nil +} diff --git a/authserver/internal/handler/context.go b/authserver/internal/handler/context.go new file mode 100644 index 0000000..dbb1acd --- /dev/null +++ b/authserver/internal/handler/context.go @@ -0,0 +1,39 @@ +package handler + +import ( + "net/http" + "strings" + + "authserver/internal/auth" + "authserver/internal/config" + + "github.com/gin-gonic/gin" +) + +const ClaimsKey = "claims" + +func AuthMiddleware(cfg config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + value := c.GetHeader("Authorization") + if !strings.HasPrefix(value, "Bearer ") { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"}) + return + } + claims, err := auth.Parse(cfg.JWTSecret, strings.TrimPrefix(value, "Bearer ")) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + c.Set(ClaimsKey, claims) + c.Next() + } +} + +func CurrentClaims(c *gin.Context) (*auth.Claims, bool) { + value, ok := c.Get(ClaimsKey) + if !ok { + return nil, false + } + claims, ok := value.(*auth.Claims) + return claims, ok +} diff --git a/authserver/internal/handler/health.go b/authserver/internal/handler/health.go new file mode 100644 index 0000000..91412b0 --- /dev/null +++ b/authserver/internal/handler/health.go @@ -0,0 +1,33 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type HealthHandler struct { + db *gorm.DB +} + +func NewHealthHandler(db *gorm.DB) *HealthHandler { + return &HealthHandler{db: db} +} + +func (h *HealthHandler) Healthz(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} + +func (h *HealthHandler) Readyz(c *gin.Context) { + sqlDB, err := h.db.DB() + if err != nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"status": "error", "error": err.Error()}) + return + } + if err := sqlDB.PingContext(c.Request.Context()); err != nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"status": "error", "error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} diff --git a/authserver/internal/handler/oauth.go b/authserver/internal/handler/oauth.go new file mode 100644 index 0000000..db61b34 --- /dev/null +++ b/authserver/internal/handler/oauth.go @@ -0,0 +1,290 @@ +package handler + +import ( + "encoding/base64" + "errors" + "net/http" + "net/url" + "strings" + "time" + + "authserver/internal/auth" + "authserver/internal/config" + "authserver/internal/model" + "authserver/internal/service" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const AuthSessionCookieName = "authserver_token" + +type OAuthHandler struct { + cfg config.Config + db *gorm.DB + audit *service.AuditService +} + +func NewOAuthHandler(cfg config.Config, db *gorm.DB, audit *service.AuditService) *OAuthHandler { + return &OAuthHandler{cfg: cfg, db: db, audit: audit} +} + +func (h *OAuthHandler) Authorize(c *gin.Context) { + if !h.oauthConfigured() { + c.JSON(http.StatusInternalServerError, gin.H{"error": "oauth client is not configured"}) + return + } + + clientID := strings.TrimSpace(c.Query("client_id")) + redirectURI := strings.TrimSpace(c.Query("redirect_uri")) + responseType := strings.TrimSpace(c.Query("response_type")) + state := c.Query("state") + + if responseType != "code" { + c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported response_type"}) + return + } + if !h.validClientRedirect(clientID, redirectURI) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid client_id or redirect_uri"}) + return + } + + user, ok := h.sessionUser(c) + if !ok { + loginURL := "/auth/api/v1/login/internal-sso?relay_state=" + url.QueryEscape(c.Request.URL.RequestURI()) + c.Redirect(http.StatusFound, loginURL) + return + } + + code, codeID, expiresAt, err := auth.SignOAuthCode(h.cfg.JWTSecret, h.cfg.JWTIssuer, h.cfg.OAuthCodeTTL(), user.ID, clientID, redirectURI) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if err := h.db.Create(&model.AccessToken{ + UserID: user.ID, + TokenID: codeID, + TokenType: "oauth_code", + ClientIP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + ExpiresAt: expiresAt, + }).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + h.audit.Write(service.AuditEntry{ + ActorUserID: user.ID, + ActorUsername: user.Username, + ClientIP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + Action: "oauth.authorize", + ResourceType: "oauth_client", + ResourceID: clientID, + Decision: "allow", + }) + + target, err := url.Parse(redirectURI) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid redirect_uri"}) + return + } + values := target.Query() + values.Set("code", code) + if state != "" { + values.Set("state", state) + } + target.RawQuery = values.Encode() + c.Redirect(http.StatusFound, target.String()) +} + +func (h *OAuthHandler) Token(c *gin.Context) { + c.Header("Cache-Control", "no-store") + c.Header("Pragma", "no-cache") + + if !h.oauthConfigured() { + c.JSON(http.StatusInternalServerError, gin.H{"error": "oauth client is not configured"}) + return + } + if err := c.Request.ParseForm(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + clientID, clientSecret := oauthClientCredentials(c) + redirectURI := strings.TrimSpace(c.PostForm("redirect_uri")) + code := strings.TrimSpace(c.PostForm("code")) + if c.PostForm("grant_type") != "authorization_code" { + c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported grant_type"}) + return + } + if clientID != h.cfg.OAuthClientID || clientSecret != h.cfg.OAuthClientSecret { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid client credentials"}) + return + } + + codeClaims, err := auth.ParseOAuthCode(h.cfg.JWTSecret, code) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid code"}) + return + } + if codeClaims.ClientID != clientID || codeClaims.RedirectURI != redirectURI || !h.validClientRedirect(clientID, redirectURI) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid code"}) + return + } + + var token string + var expiresAt time.Time + var user model.User + now := time.Now() + err = h.db.Transaction(func(tx *gorm.DB) error { + var codeRecord model.AccessToken + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("token_id = ? AND token_type = ? AND revoked = ? AND expires_at > ?", codeClaims.ID, "oauth_code", false, now). + First(&codeRecord).Error; err != nil { + return err + } + if err := tx.First(&user, "id = ? AND deleted_at IS NULL", codeClaims.UserID).Error; err != nil { + return err + } + if user.Status != "active" { + return service.ErrUserDisabled + } + + tokenID := "" + var err error + token, tokenID, expiresAt, err = auth.Sign(h.cfg.JWTSecret, h.cfg.JWTIssuer, h.cfg.JWTTTL(), user.ID, user.Username, user.Email, user.IsAdmin) + if err != nil { + return err + } + if err := tx.Model(&codeRecord).Updates(map[string]any{ + "revoked": true, + "revoked_at": &now, + }).Error; err != nil { + return err + } + return tx.Create(&model.AccessToken{ + UserID: user.ID, + TokenID: tokenID, + TokenType: "oauth_access", + ClientIP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + ExpiresAt: expiresAt, + }).Error + }) + if err != nil { + status := http.StatusBadRequest + if errors.Is(err, service.ErrUserDisabled) { + status = http.StatusUnauthorized + } + c.JSON(status, gin.H{"error": "invalid code"}) + return + } + + h.audit.Write(service.AuditEntry{ + ActorUserID: user.ID, + ActorUsername: user.Username, + ClientIP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + Action: "oauth.token", + ResourceType: "oauth_client", + ResourceID: clientID, + Decision: "allow", + }) + + c.JSON(http.StatusOK, gin.H{ + "access_token": token, + "token_type": "Bearer", + "expires_in": int(time.Until(expiresAt).Seconds()), + }) +} + +func (h *OAuthHandler) UserInfo(c *gin.Context) { + claims, err := bearerClaims(h.cfg, c.GetHeader("Authorization")) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + + var tokenRecord model.AccessToken + if err := h.db.Where("token_id = ? AND revoked = ? AND expires_at > ?", claims.ID, false, time.Now()).First(&tokenRecord).Error; err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + + var user model.User + if err := h.db.First(&user, "id = ? AND deleted_at IS NULL", claims.UserID).Error; err != nil || user.Status != "active" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + + display := strings.TrimSpace(user.DisplayName) + if display == "" { + display = user.Username + } + c.JSON(http.StatusOK, gin.H{ + "name": user.Username, + "email": user.Email, + "display": display, + }) +} + +func (h *OAuthHandler) oauthConfigured() bool { + return h.cfg.OAuthClientID != "" && h.cfg.OAuthClientSecret != "" +} + +func (h *OAuthHandler) validClientRedirect(clientID, redirectURI string) bool { + if clientID == "" || clientID != h.cfg.OAuthClientID || redirectURI == "" { + return false + } + if h.cfg.OAuthRedirectURI != "" { + return redirectURI == h.cfg.OAuthRedirectURI + } + parsed, err := url.Parse(redirectURI) + return err == nil && parsed.IsAbs() && (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host != "" +} + +func (h *OAuthHandler) sessionUser(c *gin.Context) (model.User, bool) { + tokenValue := "" + if cookie, err := c.Cookie(AuthSessionCookieName); err == nil { + tokenValue = cookie + } + if tokenValue == "" { + tokenValue = c.Query("sso_token") + } + claims, err := auth.Parse(h.cfg.JWTSecret, tokenValue) + if err != nil { + return model.User{}, false + } + + var user model.User + if err := h.db.First(&user, "id = ? AND deleted_at IS NULL", claims.UserID).Error; err != nil || user.Status != "active" { + return model.User{}, false + } + return user, true +} + +func oauthClientCredentials(c *gin.Context) (string, string) { + if id, secret, ok := c.Request.BasicAuth(); ok { + return id, secret + } + value := c.GetHeader("Authorization") + if strings.HasPrefix(value, "Basic ") { + raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(value, "Basic ")) + if err == nil { + id, secret, ok := strings.Cut(string(raw), ":") + if ok { + return id, secret + } + } + } + return strings.TrimSpace(c.PostForm("client_id")), strings.TrimSpace(c.PostForm("client_secret")) +} + +func bearerClaims(cfg config.Config, value string) (*auth.Claims, error) { + if !strings.HasPrefix(value, "Bearer ") { + return nil, errors.New("missing bearer token") + } + return auth.Parse(cfg.JWTSecret, strings.TrimSpace(strings.TrimPrefix(value, "Bearer "))) +} diff --git a/authserver/internal/handler/saml.go b/authserver/internal/handler/saml.go new file mode 100644 index 0000000..a90818d --- /dev/null +++ b/authserver/internal/handler/saml.go @@ -0,0 +1,114 @@ +package handler + +import ( + "log" + "net/http" + "net/url" + "strings" + "time" + + "authserver/internal/config" + "authserver/internal/service" + "authserver/internal/sso" + + "github.com/gin-gonic/gin" +) + +type SAMLHandler struct { + cfg config.Config + auth *service.AuthService +} + +func NewSAMLHandler(cfg config.Config, authService *service.AuthService) *SAMLHandler { + return &SAMLHandler{cfg: cfg, auth: authService} +} + +func (h *SAMLHandler) Metadata(c *gin.Context) { + data, err := sso.BuildSPMetadata(sso.MetadataConfig{ + EntityID: h.cfg.SAMLEntityID, + ACSURL: h.cfg.SAMLACSURL, + CertFile: h.cfg.SAMLSPCert, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.Data(http.StatusOK, "application/samlmetadata+xml; charset=utf-8", data) +} + +func (h *SAMLHandler) MetadataConfig(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "entity_id": h.cfg.SAMLEntityID, + "acs_url": h.cfg.SAMLACSURL, + "sp_cert_file": h.cfg.SAMLSPCert, + "sp_key_file": h.cfg.SAMLSPKey, + "idp_metadata_url": h.cfg.SAMLIDPMetaURL, + "sp_metadata_url": h.cfg.SAMLEntityID, + "metadata_generated": true, + }) +} + +func (h *SAMLHandler) Login(c *gin.Context) { + redirectURL, err := sso.BuildLoginRedirect(sso.LoginConfig{ + EntityID: h.cfg.SAMLEntityID, + ACSURL: h.cfg.SAMLACSURL, + IDPMetadataURL: h.cfg.SAMLIDPMetaURL, + RelayState: c.Query("relay_state"), + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.Redirect(http.StatusFound, redirectURL) +} + +func (h *SAMLHandler) ACS(c *gin.Context) { + if err := c.Request.ParseForm(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + info, err := sso.DecodeSAMLResponse(c.PostForm("SAMLResponse"), h.cfg.SAMLSPKey) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + log.Printf("SAML ACS debug summary:\n%s", info.JSON()) + if info.DecryptedAssertionXML != "" { + log.Printf("SAML ACS decrypted assertion:\n%s", info.DecryptedAssertionXML) + } + + result, err := h.auth.SAMLLogin(info, c.ClientIP(), c.Request.UserAgent()) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + http.SetCookie(c.Writer, &http.Cookie{ + Name: AuthSessionCookieName, + Value: result.Token, + Path: "/auth/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + MaxAge: int(time.Until(result.ExpiresAt).Seconds()), + }) + c.Redirect(http.StatusFound, ssoRedirectURL(c.PostForm("RelayState"), result.Token)) +} + +func ssoRedirectURL(relayState, token string) string { + target := strings.TrimSpace(relayState) + if target == "" || !strings.HasPrefix(target, "/auth/") { + target = "/auth/" + } + if strings.HasPrefix(target, "//") { + target = "/auth/" + } + parsed, err := url.Parse(target) + if err != nil || parsed.IsAbs() { + parsed = &url.URL{Path: "/auth/"} + } + values := parsed.Query() + values.Set("sso_token", token) + parsed.RawQuery = values.Encode() + return parsed.String() +} diff --git a/authserver/internal/handler/user.go b/authserver/internal/handler/user.go new file mode 100644 index 0000000..07e5056 --- /dev/null +++ b/authserver/internal/handler/user.go @@ -0,0 +1,27 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type UserHandler struct{} + +func NewUserHandler() *UserHandler { + return &UserHandler{} +} + +func (h *UserHandler) Me(c *gin.Context) { + claims, ok := CurrentClaims(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) + return + } + c.JSON(http.StatusOK, gin.H{ + "id": claims.UserID, + "username": claims.Username, + "email": claims.Email, + "is_admin": claims.IsAdmin, + }) +} diff --git a/authserver/internal/handler/wayen.go b/authserver/internal/handler/wayen.go new file mode 100644 index 0000000..5f2718f --- /dev/null +++ b/authserver/internal/handler/wayen.go @@ -0,0 +1,206 @@ +package handler + +import ( + "errors" + "net/http" + "strings" + + "authserver/internal/auth" + "authserver/internal/model" + "authserver/internal/service" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type selfWayenCredentialPayload struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type WayenHandler struct { + db *gorm.DB + wayen *service.WayenService + audit *service.AuditService +} + +func NewWayenHandler(db *gorm.DB, wayen *service.WayenService, audit *service.AuditService) *WayenHandler { + return &WayenHandler{db: db, wayen: wayen, audit: audit} +} + +func (h *WayenHandler) Login(c *gin.Context) { + claims, ok := CurrentClaims(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) + return + } + + email, _, ok := h.currentUser(c, claims) + if !ok { + return + } + + result, err := h.wayen.Login(email, claims.Username) + if err != nil { + h.writeAudit(c, claims.UserID, claims.Username, "deny", err.Error()) + switch { + case errors.Is(err, service.ErrWayenEmailMissing): + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + case errors.Is(err, service.ErrWayenNotConfigured): + c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()}) + case errors.Is(err, service.ErrWayenCredentialNotFound): + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + case errors.Is(err, service.ErrWayenLoginFailed): + status := http.StatusBadGateway + if strings.Contains(err.Error(), "status 400") { + status = http.StatusBadRequest + } + c.JSON(status, gin.H{"error": err.Error()}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } + return + } + + for _, cookie := range result.Cookies { + http.SetCookie(c.Writer, cookie) + } + h.writeAudit(c, claims.UserID, claims.Username, "allow", "") + if strings.Contains(c.GetHeader("Accept"), "application/json") { + c.JSON(http.StatusOK, gin.H{"target_url": result.TargetURL}) + return + } + c.Redirect(http.StatusFound, result.TargetURL) +} + +func (h *WayenHandler) GetCredential(c *gin.Context) { + claims, ok := CurrentClaims(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) + return + } + + email, _, ok := h.currentUser(c, claims) + if !ok { + return + } + + response := gin.H{ + "email": email, + "configured": false, + "enabled": false, + } + if email == "" { + c.JSON(http.StatusOK, response) + return + } + + var item model.WayenCredential + if err := h.db.Where("email = ?", email).First(&item).Error; err == nil { + response["configured"] = true + response["enabled"] = item.Enabled + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, response) +} + +func (h *WayenHandler) SaveCredential(c *gin.Context) { + claims, ok := CurrentClaims(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) + return + } + + var req selfWayenCredentialPayload + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + email, user, ok := h.currentUser(c, claims) + if !ok { + return + } + if trimmed := strings.TrimSpace(req.Email); trimmed != "" { + email = trimmed + } + if email == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "email is required"}) + return + } + password := strings.TrimSpace(req.Password) + if password == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "password is required"}) + return + } + + if err := h.db.Transaction(func(tx *gorm.DB) error { + if user.ID != 0 && strings.TrimSpace(user.Email) != email { + if err := tx.Model(&model.User{}).Where("id = ?", user.ID).Update("email", email).Error; err != nil { + return err + } + } + + var item model.WayenCredential + err := tx.Where("email = ?", email).First(&item).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return tx.Create(&model.WayenCredential{ + Email: email, + Password: password, + Enabled: true, + }).Error + } + if err != nil { + return err + } + return tx.Model(&item).Updates(map[string]any{ + "password": password, + "enabled": true, + }).Error + }); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + h.audit.Write(service.AuditEntry{ + ActorUserID: claims.UserID, + ActorUsername: claims.Username, + ClientIP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + Action: "wayen.credential.save", + ResourceType: "wayen_credential", + ResourceID: email, + Decision: "allow", + }) + c.JSON(http.StatusOK, gin.H{"email": email, "configured": true, "enabled": true}) +} + +func (h *WayenHandler) currentUser(c *gin.Context, claims *auth.Claims) (string, model.User, bool) { + var user model.User + email := strings.TrimSpace(claims.Email) + if claims.UserID != 0 { + if err := h.db.Select("id", "email").Where("id = ?", claims.UserID).First(&user).Error; err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "current user not found"}) + return "", user, false + } + if email == "" { + email = strings.TrimSpace(user.Email) + } + } + return email, user, true +} + +func (h *WayenHandler) writeAudit(c *gin.Context, userID uint64, username, decision, reason string) { + h.audit.Write(service.AuditEntry{ + ActorUserID: userID, + ActorUsername: username, + ClientIP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + Action: "wayen.login", + ResourceType: "wayen", + Decision: decision, + Reason: reason, + }) +} diff --git a/authserver/internal/model/models.go b/authserver/internal/model/models.go new file mode 100644 index 0000000..c6a052f --- /dev/null +++ b/authserver/internal/model/models.go @@ -0,0 +1,68 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +type User struct { + ID uint64 `gorm:"primaryKey" json:"id"` + Username string `gorm:"size:128;not null;uniqueIndex" json:"username"` + DisplayName string `gorm:"size:128;not null;default:''" json:"display_name"` + Email string `gorm:"size:255;not null;default:'';index" json:"email"` + Phone string `gorm:"size:32;not null;default:''" json:"phone"` + Source string `gorm:"size:32;not null;default:'local';index:idx_users_external,priority:1" json:"source"` + ExternalID string `gorm:"size:128;not null;default:'';index:idx_users_external,priority:2" json:"external_id"` + Status string `gorm:"size:32;not null;default:'active';index" json:"status"` + IsAdmin bool `gorm:"not null;default:false" json:"is_admin"` + LastLoginAt *time.Time `json:"last_login_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +type WayenCredential struct { + ID uint64 `gorm:"primaryKey" json:"id"` + Email string `gorm:"size:255;not null;uniqueIndex" json:"email"` + Password string `gorm:"size:512;not null" json:"-"` + Enabled bool `gorm:"not null;default:true;index" json:"enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +type AccessToken struct { + ID uint64 `gorm:"primaryKey" json:"id"` + UserID uint64 `gorm:"not null;index" json:"user_id"` + TokenID string `gorm:"size:128;not null;uniqueIndex" json:"token_id"` + TokenType string `gorm:"size:32;not null;default:'access'" json:"token_type"` + ClientIP string `gorm:"size:64;not null;default:''" json:"client_ip"` + UserAgent string `gorm:"size:512;not null;default:''" json:"user_agent"` + Revoked bool `gorm:"not null;default:false;index" json:"revoked"` + ExpiresAt time.Time `gorm:"not null;index" json:"expires_at"` + CreatedAt time.Time `json:"created_at"` + RevokedAt *time.Time `json:"revoked_at"` +} + +type AuditLog struct { + ID uint64 `gorm:"primaryKey" json:"id"` + RequestID string `gorm:"size:128;not null;default:''" json:"request_id"` + ActorUserID uint64 `gorm:"not null;default:0;index:idx_actor_time,priority:1" json:"actor_user_id"` + ActorUsername string `gorm:"size:128;not null;default:''" json:"actor_username"` + ClientIP string `gorm:"size:64;not null;default:''" json:"client_ip"` + UserAgent string `gorm:"size:512;not null;default:''" json:"user_agent"` + Action string `gorm:"size:128;not null;index:idx_action_time,priority:1" json:"action"` + ResourceType string `gorm:"size:64;not null;default:'';index:idx_resource,priority:1" json:"resource_type"` + ResourceID string `gorm:"size:128;not null;default:'';index:idx_resource,priority:2" json:"resource_id"` + ScopeType string `gorm:"size:32;not null;default:'';index:idx_scope,priority:1" json:"scope_type"` + ScopeID uint64 `gorm:"not null;default:0;index:idx_scope,priority:2" json:"scope_id"` + BusinessLineID uint64 `gorm:"not null;default:0" json:"business_line_id"` + NamespaceID uint64 `gorm:"not null;default:0;index:idx_ns_cluster_env_time,priority:1" json:"namespace_id"` + ClusterID uint64 `gorm:"not null;default:0;index:idx_ns_cluster_env_time,priority:2" json:"cluster_id"` + EnvironmentID uint64 `gorm:"not null;default:0;index:idx_ns_cluster_env_time,priority:3" json:"environment_id"` + Decision string `gorm:"size:32;not null;default:'';index:idx_decision_time,priority:1" json:"decision"` + Reason string `gorm:"size:512;not null;default:''" json:"reason"` + Metadata string `gorm:"type:longtext" json:"metadata"` + CreatedAt time.Time `gorm:"index:idx_actor_time,priority:2;index:idx_action_time,priority:2;index:idx_ns_cluster_env_time,priority:4;index:idx_decision_time,priority:2" json:"created_at"` +} diff --git a/authserver/internal/router/router.go b/authserver/internal/router/router.go new file mode 100644 index 0000000..01556c4 --- /dev/null +++ b/authserver/internal/router/router.go @@ -0,0 +1,56 @@ +package router + +import ( + "authserver/internal/config" + "authserver/internal/handler" + "authserver/internal/service" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type Dependencies struct { + Config config.Config + DB *gorm.DB +} + +func New(deps Dependencies) *gin.Engine { + if deps.Config.AppEnv == "prod" { + gin.SetMode(gin.ReleaseMode) + } + + r := gin.New() + r.Use(gin.Logger(), gin.Recovery()) + + auditService := service.NewAuditService(deps.DB) + authService := service.NewAuthService(deps.Config, deps.DB, auditService) + wayenService := service.NewWayenService(deps.Config, deps.DB) + + healthHandler := handler.NewHealthHandler(deps.DB) + userHandler := handler.NewUserHandler() + wayenHandler := handler.NewWayenHandler(deps.DB, wayenService, auditService) + samlHandler := handler.NewSAMLHandler(deps.Config, authService) + oauthHandler := handler.NewOAuthHandler(deps.Config, deps.DB, auditService) + + r.GET("/healthz", healthHandler.Healthz) + r.GET("/readyz", healthHandler.Readyz) + r.GET("/auth/oauth/authorize", oauthHandler.Authorize) + r.POST("/auth/oauth/token", oauthHandler.Token) + r.GET("/auth/oauth/userinfo", oauthHandler.UserInfo) + + v1 := r.Group("/auth/api/v1") + { + v1.GET("/login/internal-sso", samlHandler.Login) + v1.GET("/saml/metadata", samlHandler.Metadata) + v1.POST("/saml/acs", samlHandler.ACS) + + protected := v1.Group("") + protected.Use(handler.AuthMiddleware(deps.Config)) + protected.GET("/users/me", userHandler.Me) + protected.GET("/wayen/login", wayenHandler.Login) + protected.GET("/wayen/credential", wayenHandler.GetCredential) + protected.PUT("/wayen/credential", wayenHandler.SaveCredential) + } + + return r +} diff --git a/authserver/internal/service/audit.go b/authserver/internal/service/audit.go new file mode 100644 index 0000000..60fde17 --- /dev/null +++ b/authserver/internal/service/audit.go @@ -0,0 +1,60 @@ +package service + +import ( + "encoding/json" + + "authserver/internal/model" + + "gorm.io/gorm" +) + +type AuditService struct { + db *gorm.DB +} + +func NewAuditService(db *gorm.DB) *AuditService { + return &AuditService{db: db} +} + +type AuditEntry struct { + RequestID string + ActorUserID uint64 + ActorUsername string + ClientIP string + UserAgent string + Action string + ResourceType string + ResourceID string + ScopeType string + ScopeID uint64 + BusinessLineID uint64 + NamespaceID uint64 + ClusterID uint64 + EnvironmentID uint64 + Decision string + Reason string + Metadata map[string]any +} + +func (s *AuditService) Write(entry AuditEntry) { + raw, _ := json.Marshal(entry.Metadata) + _ = s.db.Create(&model.AuditLog{ + RequestID: entry.RequestID, + ActorUserID: entry.ActorUserID, + ActorUsername: entry.ActorUsername, + ClientIP: entry.ClientIP, + UserAgent: entry.UserAgent, + Action: entry.Action, + ResourceType: entry.ResourceType, + ResourceID: entry.ResourceID, + ScopeType: entry.ScopeType, + ScopeID: entry.ScopeID, + BusinessLineID: entry.BusinessLineID, + NamespaceID: entry.NamespaceID, + ClusterID: entry.ClusterID, + EnvironmentID: entry.EnvironmentID, + Decision: entry.Decision, + Reason: entry.Reason, + Metadata: string(raw), + }).Error +} diff --git a/authserver/internal/service/auth.go b/authserver/internal/service/auth.go new file mode 100644 index 0000000..91b8f62 --- /dev/null +++ b/authserver/internal/service/auth.go @@ -0,0 +1,245 @@ +package service + +import ( + "errors" + "fmt" + "net/mail" + "regexp" + "strings" + "time" + + "authserver/internal/auth" + "authserver/internal/config" + "authserver/internal/model" + "authserver/internal/sso" + + "gorm.io/gorm" +) + +var ( + ErrInvalidCredential = errors.New("invalid username or password") + ErrUserDisabled = errors.New("user disabled") + ErrSAMLSubjectMissing = errors.New("saml subject is missing") +) + +type AuthService struct { + cfg config.Config + db *gorm.DB + audit *AuditService +} + +type LoginResult struct { + Token string `json:"token"` + TokenID string `json:"token_id"` + ExpiresAt time.Time `json:"expires_at"` + User model.User `json:"user"` +} + +func NewAuthService(cfg config.Config, db *gorm.DB, audit *AuditService) *AuthService { + return &AuthService{cfg: cfg, db: db, audit: audit} +} + +func (s *AuthService) SAMLLogin(info *sso.SAMLDebugInfo, clientIP, userAgent string) (*LoginResult, error) { + subject := strings.TrimSpace(info.NameID) + email := firstSAMLAttribute(info.Attributes, + "email", + "mail", + "Email", + "EmailAddress", + "emailAddress", + "urn:oid:0.9.2342.19200300.100.1.3", + "urn:oid:1.3.6.1.4.1.5923.1.1.1.6", + ) + if email == "" && looksLikeEmail(subject) { + email = subject + } + if subject == "" { + subject = email + } + if subject == "" { + reason := fmt.Sprintf("subject_missing name_id=%q attribute_keys=%v", info.NameID, samlAttributeKeys(info.Attributes)) + s.audit.Write(AuditEntry{ClientIP: clientIP, UserAgent: userAgent, Action: "saml.login.failed", Decision: "deny", Reason: reason}) + return nil, ErrSAMLSubjectMissing + } + + displayName := firstSAMLAttribute(info.Attributes, + "displayName", + "name", + "cn", + "username", + "uid", + "urn:oid:2.5.4.3", + "urn:oid:0.9.2342.19200300.100.1.1", + ) + + var result *LoginResult + err := s.db.Transaction(func(tx *gorm.DB) error { + user, err := findOrCreateSAMLUser(tx, subject, email, displayName) + if err != nil { + return err + } + if user.Status != "active" { + s.audit.Write(AuditEntry{ActorUserID: user.ID, ActorUsername: user.Username, ClientIP: clientIP, UserAgent: userAgent, Action: "saml.login.failed", Decision: "deny", Reason: "user_disabled"}) + return ErrUserDisabled + } + + token, tokenID, expiresAt, err := auth.Sign(s.cfg.JWTSecret, s.cfg.JWTIssuer, s.cfg.JWTTTL(), user.ID, user.Username, user.Email, user.IsAdmin) + if err != nil { + return err + } + + now := time.Now() + if err := tx.Model(&model.User{}).Where("id = ?", user.ID).Updates(map[string]any{ + "last_login_at": now, + "email": user.Email, + "display_name": user.DisplayName, + }).Error; err != nil { + return err + } + if err := tx.Create(&model.AccessToken{ + UserID: user.ID, + TokenID: tokenID, + TokenType: "access", + ClientIP: clientIP, + UserAgent: userAgent, + ExpiresAt: expiresAt, + }).Error; err != nil { + return err + } + + user.LastLoginAt = &now + result = &LoginResult{Token: token, TokenID: tokenID, ExpiresAt: expiresAt, User: user} + return nil + }) + if err != nil { + return nil, err + } + + s.audit.Write(AuditEntry{ActorUserID: result.User.ID, ActorUsername: result.User.Username, ClientIP: clientIP, UserAgent: userAgent, Action: "saml.login.success", Decision: "allow"}) + return result, nil +} + +func findOrCreateSAMLUser(tx *gorm.DB, subject, email, displayName string) (model.User, error) { + username := samlUsername(email, subject) + if displayName == "" { + displayName = username + } + + var user model.User + if err := findExistingSAMLUser(tx, &user, username, email, subject); err == nil { + return updateSAMLUser(tx, user, username, email, displayName, subject) + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return model.User{}, err + } + + user = model.User{ + Username: username, + DisplayName: displayName, + Email: email, + Source: "saml", + ExternalID: subject, + Status: "active", + IsAdmin: false, + } + if err := tx.Create(&user).Error; err != nil { + return user, err + } + return user, nil +} + +func findExistingSAMLUser(tx *gorm.DB, user *model.User, username, email, subject string) error { + query := tx.Where("username = ? AND deleted_at IS NULL", username) + if email != "" { + query = query.Or("email = ? AND deleted_at IS NULL", email) + } + if subject != "" { + query = query.Or("source = ? AND external_id = ? AND deleted_at IS NULL", "saml", subject) + } + return query.First(user).Error +} + +func updateSAMLUser(tx *gorm.DB, user model.User, username, email, displayName, subject string) (model.User, error) { + updates := map[string]any{ + "source": "saml", + "external_id": subject, + } + user.Source = "saml" + user.ExternalID = subject + + if email != "" && user.Email != email { + updates["email"] = email + user.Email = email + } + if displayName != "" && user.DisplayName != displayName { + updates["display_name"] = displayName + user.DisplayName = displayName + } + if username != "" && user.Username != username { + var count int64 + if err := tx.Model(&model.User{}).Where("username = ? AND id <> ? AND deleted_at IS NULL", username, user.ID).Count(&count).Error; err != nil { + return user, err + } + if count == 0 { + updates["username"] = username + user.Username = username + } + } + if err := tx.Model(&model.User{}).Where("id = ?", user.ID).Updates(updates).Error; err != nil { + return user, err + } + return user, nil +} + +func firstSAMLAttribute(attrs map[string][]string, names ...string) string { + for _, name := range names { + for _, value := range attrs[name] { + value = strings.TrimSpace(value) + if value != "" { + return value + } + } + } + for key, values := range attrs { + lower := strings.ToLower(key) + if strings.Contains(lower, "email") || strings.Contains(lower, "mail") { + for _, value := range values { + value = strings.TrimSpace(value) + if value != "" { + return value + } + } + } + } + return "" +} + +func looksLikeEmail(value string) bool { + if value == "" { + return false + } + _, err := mail.ParseAddress(value) + return err == nil && strings.Contains(value, "@") +} + +var usernameCleaner = regexp.MustCompile(`[^a-zA-Z0-9@._-]+`) + +func samlUsername(email, subject string) string { + base := strings.TrimSpace(email) + if base == "" { + base = strings.TrimSpace(subject) + } + base = usernameCleaner.ReplaceAllString(base, "-") + base = strings.Trim(base, ".-_") + if base == "" { + return "saml-user" + } + return base +} + +func samlAttributeKeys(attrs map[string][]string) []string { + keys := make([]string, 0, len(attrs)) + for key := range attrs { + keys = append(keys, key) + } + return keys +} diff --git a/authserver/internal/service/wayen.go b/authserver/internal/service/wayen.go new file mode 100644 index 0000000..a21f2d3 --- /dev/null +++ b/authserver/internal/service/wayen.go @@ -0,0 +1,143 @@ +package service + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "authserver/internal/config" + "authserver/internal/model" + + "gorm.io/gorm" +) + +var ( + ErrWayenNotConfigured = errors.New("wayen login is not configured") + ErrWayenEmailMissing = errors.New("email is missing in token") + ErrWayenCredentialNotFound = errors.New("wayen credential not found") + ErrWayenLoginFailed = errors.New("wayen login failed") +) + +type WayenService struct { + cfg config.Config + db *gorm.DB + client *http.Client +} + +type WayenLoginResult struct { + TargetURL string + Cookies []*http.Cookie +} + +func NewWayenService(cfg config.Config, db *gorm.DB) *WayenService { + return &WayenService{ + cfg: cfg, + db: db, + client: &http.Client{ + Timeout: 10 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + } +} + +func (s *WayenService) Login(email, username string) (*WayenLoginResult, error) { + email = strings.TrimSpace(email) + if email == "" { + return nil, ErrWayenEmailMissing + } + if strings.TrimSpace(s.cfg.WayenLoginURL) == "" || strings.TrimSpace(s.cfg.WayenTargetURL) == "" { + return nil, ErrWayenNotConfigured + } + + var credential model.WayenCredential + if err := s.db.Where("email = ? AND enabled = ?", email, true).First(&credential).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrWayenCredentialNotFound + } + return nil, err + } + + loginName := email + if strings.EqualFold(s.cfg.WayenLoginValue, "username") && strings.TrimSpace(username) != "" { + loginName = strings.TrimSpace(username) + } + + loginURL, body, contentType, err := s.loginRequest(s.cfg.WayenLoginURL, loginName, credential.Password) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodPost, loginURL, body) + if err != nil { + return nil, err + } + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + req.Header.Set("Accept", "application/json, text/html;q=0.9, */*;q=0.8") + + resp, err := s.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<20)) + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest { + return nil, fmt.Errorf("%w: status %d", ErrWayenLoginFailed, resp.StatusCode) + } + + return &WayenLoginResult{ + TargetURL: s.cfg.WayenTargetURL, + Cookies: resp.Cookies(), + }, nil +} + +func (s *WayenService) loginRequest(loginURL, email, password string) (string, io.Reader, string, error) { + usernameKey := defaultConfigValue(s.cfg.WayenUsernameKey, "email") + passwordKey := defaultConfigValue(s.cfg.WayenPasswordKey, "password") + + if strings.EqualFold(s.cfg.WayenLoginFormat, "query") { + parsed, err := url.Parse(loginURL) + if err != nil { + return "", nil, "", err + } + values := parsed.Query() + values.Set(usernameKey, email) + values.Set(passwordKey, password) + parsed.RawQuery = values.Encode() + return parsed.String(), nil, "", nil + } + + if strings.EqualFold(s.cfg.WayenLoginFormat, "json") { + payload := map[string]string{ + usernameKey: email, + passwordKey: password, + } + data, err := json.Marshal(payload) + if err != nil { + return "", nil, "", err + } + return loginURL, bytes.NewReader(data), "application/json", nil + } + + values := url.Values{} + values.Set(usernameKey, email) + values.Set(passwordKey, password) + return loginURL, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded", nil +} + +func defaultConfigValue(value, fallback string) string { + value = strings.TrimSpace(value) + if value == "" { + return fallback + } + return value +} diff --git a/authserver/internal/sso/login.go b/authserver/internal/sso/login.go new file mode 100644 index 0000000..c863fe4 --- /dev/null +++ b/authserver/internal/sso/login.go @@ -0,0 +1,415 @@ +package sso + +import ( + "bytes" + "compress/flate" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "encoding/xml" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +const ( + samlHTTPRedirectBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + samlHTTPPostBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" +) + +type LoginConfig struct { + EntityID string + ACSURL string + IDPMetadataURL string + RelayState string +} + +func BuildLoginRedirect(cfg LoginConfig) (string, error) { + if cfg.EntityID == "" { + return "", errors.New("saml entity id is required") + } + if cfg.ACSURL == "" { + return "", errors.New("saml acs url is required") + } + ssoURL, err := ReadIDPSSOURL(cfg.IDPMetadataURL) + if err != nil { + return "", err + } + request, err := buildAuthnRequest(cfg.EntityID, cfg.ACSURL, ssoURL) + if err != nil { + return "", err + } + encoded, err := deflateAndBase64(request) + if err != nil { + return "", err + } + redirectURL, err := url.Parse(ssoURL) + if err != nil { + return "", err + } + query := redirectURL.Query() + query.Set("SAMLRequest", encoded) + if cfg.RelayState != "" { + query.Set("RelayState", cfg.RelayState) + } + redirectURL.RawQuery = query.Encode() + return redirectURL.String(), nil +} + +func ReadIDPSSOURL(metadataURL string) (string, error) { + if metadataURL == "" { + return "", errors.New("saml idp metadata url is required") + } + request, err := http.NewRequest(http.MethodGet, metadataURL, nil) + if err != nil { + return "", err + } + request.Header.Set("Accept", "application/samlmetadata+xml, application/xml, text/xml, */*") + request.Header.Set("User-Agent", "authserver-saml-metadata/1.0") + + client := &http.Client{Timeout: 10 * time.Second} + response, err := client.Do(request) + if err != nil { + return "", err + } + defer response.Body.Close() + raw, err := io.ReadAll(io.LimitReader(response.Body, 1<<20)) + if err != nil { + return "", err + } + if response.StatusCode != http.StatusOK { + return "", fmt.Errorf("saml idp metadata request failed: url=%s status=%s body=%q", request.URL.String(), response.Status, truncateForLog(string(raw), 300)) + } + return readIDPSSOURL(raw) +} + +func truncateForLog(value string, limit int) string { + value = strings.TrimSpace(value) + if limit <= 0 || len(value) <= limit { + return value + } + return value[:limit] + "...(truncated)" +} + +func readIDPSSOURL(raw []byte) (string, error) { + var doc idpEntityDescriptor + if err := xml.Unmarshal(raw, &doc); err != nil { + return "", err + } + for _, service := range doc.IDPSSODescriptor.SingleSignOnServices { + if service.Binding == samlHTTPRedirectBinding && service.Location != "" { + return service.Location, nil + } + } + for _, service := range doc.IDPSSODescriptor.SingleSignOnServices { + if service.Location != "" { + return service.Location, nil + } + } + return "", errors.New("saml idp metadata has no single sign on service") +} + +func DecodeSAMLResponse(value, privateKeyFile string) (*SAMLDebugInfo, error) { + if value == "" { + return nil, errors.New("SAMLResponse is required") + } + raw, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return nil, err + } + info := &SAMLDebugInfo{ + RawXML: string(raw), + } + _ = xml.Unmarshal(raw, &info.Response) + if info.Response.Assertion.Subject.NameID.Value == "" && info.Response.EncryptedAssertion.EncryptedData.CipherData.CipherValue != "" { + assertionXML, assertion, err := decryptEncryptedAssertion(info.Response.EncryptedAssertion, privateKeyFile) + if err != nil { + return nil, err + } + info.DecryptedAssertionXML = assertionXML + info.Response.EncryptedAssertion.Assertion = assertion + } + info.NameID = firstNameID(info.Response.Assertion.Subject.NameID.Value, info.Response.EncryptedAssertion.Assertion.Subject.NameID.Value) + info.Attributes = make(map[string][]string) + for _, attr := range info.Response.Assertion.AttributeStatement.Attributes { + addSAMLAttributeValues(info.Attributes, attr) + } + for _, attr := range info.Response.EncryptedAssertion.Assertion.AttributeStatement.Attributes { + addSAMLAttributeValues(info.Attributes, attr) + } + return info, nil +} + +func addSAMLAttributeValues(attrs map[string][]string, attr attribute) { + if attr.Name != "" { + attrs[attr.Name] = append(attrs[attr.Name], attr.Values...) + } + if attr.FriendlyName != "" && attr.FriendlyName != attr.Name { + attrs[attr.FriendlyName] = append(attrs[attr.FriendlyName], attr.Values...) + } +} + +func decryptEncryptedAssertion(encrypted encryptedAssertion, privateKeyFile string) (string, assertion, error) { + key, err := readRSAPrivateKey(privateKeyFile) + if err != nil { + return "", assertion{}, err + } + encryptedKey, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encrypted.EncryptedData.EncryptedKey.CipherData.CipherValue)) + if err != nil { + return "", assertion{}, fmt.Errorf("decode saml encrypted key: %w", err) + } + sessionKey, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, key, encryptedKey, nil) + if err != nil { + return "", assertion{}, fmt.Errorf("decrypt saml encrypted key: %w", err) + } + encryptedValue, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encrypted.EncryptedData.CipherData.CipherValue)) + if err != nil { + return "", assertion{}, fmt.Errorf("decode saml encrypted assertion: %w", err) + } + plain, err := decryptAESCBC(encryptedValue, sessionKey) + if err != nil { + return "", assertion{}, err + } + assertionXML := string(plain) + var out assertion + if err := xml.Unmarshal(plain, &out); err != nil { + return "", assertion{}, fmt.Errorf("parse decrypted saml assertion: %w", err) + } + return assertionXML, out, nil +} + +func readRSAPrivateKey(path string) (*rsa.PrivateKey, error) { + if strings.TrimSpace(path) == "" { + return nil, errors.New("saml sp key file is required for encrypted assertion") + } + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + block, _ := pem.Decode(raw) + if block == nil { + return nil, errors.New("saml sp key file has no PEM block") + } + if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { + return key, nil + } + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + key, ok := parsed.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("saml sp key is not an RSA private key") + } + return key, nil +} + +func decryptAESCBC(value, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("init saml assertion cipher: %w", err) + } + if len(value) < block.BlockSize()*2 || len(value)%block.BlockSize() != 0 { + return nil, errors.New("invalid saml encrypted assertion length") + } + iv := value[:block.BlockSize()] + cipherText := value[block.BlockSize():] + plain := make([]byte, len(cipherText)) + cipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, cipherText) + plain, err = pkcs7Unpad(plain, block.BlockSize()) + if err != nil { + return nil, err + } + return plain, nil +} + +func pkcs7Unpad(value []byte, blockSize int) ([]byte, error) { + if len(value) == 0 || len(value)%blockSize != 0 { + return nil, errors.New("invalid saml assertion padding length") + } + padding := int(value[len(value)-1]) + if padding == 0 || padding > blockSize || padding > len(value) { + return nil, errors.New("invalid saml assertion padding") + } + for _, b := range value[len(value)-padding:] { + if int(b) != padding { + return nil, errors.New("invalid saml assertion padding bytes") + } + } + return value[:len(value)-padding], nil +} + +func (i *SAMLDebugInfo) JSON() string { + data, _ := json.MarshalIndent(map[string]any{ + "id": i.Response.ID, + "in_response_to": i.Response.InResponseTo, + "issuer": i.Response.Issuer, + "destination": i.Response.Destination, + "name_id": i.NameID, + "attributes": i.Attributes, + }, "", " ") + return string(data) +} + +func buildAuthnRequest(entityID, acsURL, destination string) ([]byte, error) { + req := authnRequest{ + XMLName: xml.Name{Local: "samlp:AuthnRequest"}, + XmlnsSAMLp: samlProtocolNS, + XmlnsSAML: "urn:oasis:names:tc:SAML:2.0:assertion", + ID: "_" + randomHex(20), + Version: "2.0", + IssueInstant: time.Now().UTC().Format(time.RFC3339), + Destination: destination, + AssertionConsumerServiceURL: acsURL, + ProtocolBinding: samlHTTPPostBinding, + Issuer: issuer{ + Value: entityID, + }, + NameIDPolicy: nameIDPolicy{ + Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", + AllowCreate: true, + }, + } + return xml.Marshal(req) +} + +func deflateAndBase64(raw []byte) (string, error) { + var buf bytes.Buffer + writer, err := flate.NewWriter(&buf, flate.DefaultCompression) + if err != nil { + return "", err + } + if _, err := writer.Write(raw); err != nil { + return "", err + } + if err := writer.Close(); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(buf.Bytes()), nil +} + +func randomHex(size int) string { + buf := make([]byte, size) + if _, err := rand.Read(buf); err != nil { + return fmt.Sprintf("%d", time.Now().UnixNano()) + } + return hex.EncodeToString(buf) +} + +func firstNameID(values ...string) string { + for _, value := range values { + value = strings.TrimSpace(value) + if value != "" { + return value + } + } + return "" +} + +type idpEntityDescriptor struct { + IDPSSODescriptor struct { + SingleSignOnServices []struct { + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + } `xml:"SingleSignOnService"` + } `xml:"IDPSSODescriptor"` +} + +type authnRequest struct { + XMLName xml.Name `xml:"samlp:AuthnRequest"` + XmlnsSAMLp string `xml:"xmlns:samlp,attr"` + XmlnsSAML string `xml:"xmlns:saml,attr"` + ID string `xml:"ID,attr"` + Version string `xml:"Version,attr"` + IssueInstant string `xml:"IssueInstant,attr"` + Destination string `xml:"Destination,attr"` + AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr"` + ProtocolBinding string `xml:"ProtocolBinding,attr"` + Issuer issuer `xml:"saml:Issuer"` + NameIDPolicy nameIDPolicy `xml:"samlp:NameIDPolicy"` +} + +type issuer struct { + Value string `xml:",chardata"` +} + +type nameIDPolicy struct { + Format string `xml:"Format,attr"` + AllowCreate bool `xml:"AllowCreate,attr"` +} + +type SAMLDebugInfo struct { + RawXML string + DecryptedAssertionXML string + NameID string + Attributes map[string][]string + Response samlResponse +} + +type samlResponse struct { + ID string `xml:"ID,attr"` + InResponseTo string `xml:"InResponseTo,attr"` + Destination string `xml:"Destination,attr"` + Issuer string `xml:"Issuer"` + Assertion assertion `xml:"Assertion"` + EncryptedAssertion encryptedAssertion `xml:"EncryptedAssertion"` +} + +type encryptedAssertion struct { + Assertion assertion `xml:"Assertion"` + EncryptedData encryptedData `xml:"EncryptedData"` +} + +type encryptedData struct { + EncryptionMethod struct { + Algorithm string `xml:"Algorithm,attr"` + } `xml:"EncryptionMethod"` + EncryptedKey encryptedKey `xml:"KeyInfo>EncryptedKey"` + CipherData cipherData `xml:"CipherData"` +} + +type encryptedKey struct { + EncryptionMethod struct { + Algorithm string `xml:"Algorithm,attr"` + } `xml:"EncryptionMethod"` + CipherData cipherData `xml:"CipherData"` +} + +type cipherData struct { + CipherValue string `xml:"CipherValue"` +} + +type assertion struct { + Subject subject `xml:"Subject"` + AttributeStatement attributeStatement `xml:"AttributeStatement"` +} + +type subject struct { + NameID nameID `xml:"NameID"` +} + +type nameID struct { + Value string `xml:",chardata"` +} + +type attributeStatement struct { + Attributes []attribute `xml:"Attribute"` +} + +type attribute struct { + Name string `xml:"Name,attr"` + FriendlyName string `xml:"FriendlyName,attr"` + Values []string `xml:"AttributeValue"` +} diff --git a/authserver/internal/sso/login_test.go b/authserver/internal/sso/login_test.go new file mode 100644 index 0000000..eb1d3c3 --- /dev/null +++ b/authserver/internal/sso/login_test.go @@ -0,0 +1,190 @@ +package sso + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" +) + +func TestReadIDPSSOURLFetchesMetadataURL(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Fatalf("unexpected method: %s", r.Method) + } + _, _ = w.Write([]byte(testIDPMetadataXML( + "https://idp.example.com/saml2/meta", + "https://idp.example.com/sso/redirect", + "https://idp.example.com/sso/post", + ))) + })) + defer server.Close() + + got, err := ReadIDPSSOURL(server.URL) + if err != nil { + t.Fatalf("ReadIDPSSOURL returned error: %v", err) + } + if got != "https://idp.example.com/sso/redirect" { + t.Fatalf("unexpected sso url: %s", got) + } +} + +func TestReadIDPSSOURLReturnsHTTPError(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "boom", http.StatusBadGateway) + })) + defer server.Close() + + if _, err := ReadIDPSSOURL(server.URL); err == nil { + t.Fatal("expected error for non-200 metadata response") + } +} + +func TestBuildLoginRedirectUsesRemoteMetadata(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(testIDPMetadataXML( + "https://idp.example.com/saml2/meta", + "https://idp.example.com/sso/redirect", + "https://idp.example.com/sso/post", + ))) + })) + defer server.Close() + + redirectURL, err := BuildLoginRedirect(LoginConfig{ + EntityID: "https://sp.example.com/auth/api/v1/saml/metadata", + ACSURL: "https://sp.example.com/auth/api/v1/saml/acs", + IDPMetadataURL: server.URL, + RelayState: "/auth/", + }) + if err != nil { + t.Fatalf("BuildLoginRedirect returned error: %v", err) + } + + parsed, err := url.Parse(redirectURL) + if err != nil { + t.Fatalf("parse redirect url: %v", err) + } + if parsed.String() == "" { + t.Fatal("redirect url is empty") + } + if parsed.Scheme != "https" || parsed.Host != "idp.example.com" || parsed.Path != "/sso/redirect" { + t.Fatalf("unexpected redirect target: %s", redirectURL) + } + if parsed.Query().Get("SAMLRequest") == "" { + t.Fatal("missing SAMLRequest query") + } + if parsed.Query().Get("RelayState") != "/auth/" { + t.Fatalf("unexpected relay state: %s", parsed.Query().Get("RelayState")) + } +} + +func TestDecodeSAMLResponseDecryptsEncryptedAssertion(t *testing.T) { + t.Parallel() + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("generate key: %v", err) + } + keyFile := writeTestPrivateKey(t, key) + + assertion := ` + alice@example.com + + alice@example.com + +` + sessionKey := []byte("0123456789abcdef") + encryptedAssertion := encryptTestAssertion(t, []byte(assertion), sessionKey) + encryptedKey, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, &key.PublicKey, sessionKey, nil) + if err != nil { + t.Fatalf("encrypt key: %v", err) + } + response := ` + + + + + ` + base64.StdEncoding.EncodeToString(encryptedKey) + ` + + + ` + base64.StdEncoding.EncodeToString(encryptedAssertion) + ` + + +` + + info, err := DecodeSAMLResponse(base64.StdEncoding.EncodeToString([]byte(response)), keyFile) + if err != nil { + t.Fatalf("DecodeSAMLResponse returned error: %v", err) + } + if info.NameID != "alice@example.com" { + t.Fatalf("unexpected name id: %q", info.NameID) + } + if got := info.Attributes["email"]; len(got) != 1 || got[0] != "alice@example.com" { + t.Fatalf("unexpected email attribute: %#v", got) + } + if info.DecryptedAssertionXML == "" { + t.Fatal("missing decrypted assertion xml") + } +} + +func writeTestPrivateKey(t *testing.T, key *rsa.PrivateKey) string { + t.Helper() + file, err := os.CreateTemp(t.TempDir(), "sp-*.key") + if err != nil { + t.Fatalf("create key file: %v", err) + } + err = pem.Encode(file, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + closeErr := file.Close() + if err != nil { + t.Fatalf("write key file: %v", err) + } + if closeErr != nil { + t.Fatalf("close key file: %v", closeErr) + } + return file.Name() +} + +func encryptTestAssertion(t *testing.T, plain, key []byte) []byte { + t.Helper() + block, err := aes.NewCipher(key) + if err != nil { + t.Fatalf("init cipher: %v", err) + } + plain = pkcs7Pad(plain, block.BlockSize()) + iv := bytes.Repeat([]byte{1}, block.BlockSize()) + out := make([]byte, len(iv)+len(plain)) + copy(out, iv) + cipher.NewCBCEncrypter(block, iv).CryptBlocks(out[len(iv):], plain) + return out +} + +func pkcs7Pad(value []byte, blockSize int) []byte { + padding := blockSize - len(value)%blockSize + return append(value, bytes.Repeat([]byte{byte(padding)}, padding)...) +} + +func testIDPMetadataXML(entityID, redirectURL, postURL string) string { + return ` + + + + + +` +} diff --git a/authserver/internal/sso/metadata.go b/authserver/internal/sso/metadata.go new file mode 100644 index 0000000..5752ad3 --- /dev/null +++ b/authserver/internal/sso/metadata.go @@ -0,0 +1,147 @@ +package sso + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "encoding/xml" + "errors" + "os" + "strings" +) + +const ( + samlMetadataNS = "urn:oasis:names:tc:SAML:2.0:metadata" + samlProtocolNS = "urn:oasis:names:tc:SAML:2.0:protocol" + xmlDsigNS = "http://www.w3.org/2000/09/xmldsig#" +) + +type MetadataConfig struct { + EntityID string + ACSURL string + CertFile string +} + +func BuildSPMetadata(cfg MetadataConfig) ([]byte, error) { + if cfg.EntityID == "" { + return nil, errors.New("saml entity id is required") + } + if cfg.ACSURL == "" { + return nil, errors.New("saml acs url is required") + } + cert, err := readCertificate(cfg.CertFile) + if err != nil { + return nil, err + } + + doc := entityDescriptor{ + XMLName: xml.Name{Local: "EntityDescriptor"}, + Xmlns: samlMetadataNS, + EntityID: cfg.EntityID, + SPSSODescriptor: spSSODescriptor{ + ProtocolSupportEnumeration: samlProtocolNS, + AuthnRequestsSigned: true, + WantAssertionsSigned: true, + KeyDescriptors: []keyDescriptor{ + newKeyDescriptor("signing", cert), + newKeyDescriptor("encryption", cert), + }, + NameIDFormats: []string{ + "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + }, + AssertionConsumerServices: []assertionConsumerService{ + { + Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + Location: cfg.ACSURL, + Index: 0, + IsDefault: true, + }, + }, + }, + } + + body, err := xml.MarshalIndent(doc, "", " ") + if err != nil { + return nil, err + } + out := append([]byte(xml.Header), body...) + out = append(out, '\n') + return out, nil +} + +func readCertificate(path string) (string, error) { + if path == "" { + return "", errors.New("saml sp cert file is required") + } + raw, err := os.ReadFile(path) + if err != nil { + return "", err + } + block, _ := pem.Decode(raw) + if block == nil { + return stripPEMCertificate(string(raw)), nil + } + if _, err := x509.ParseCertificate(block.Bytes); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(block.Bytes), nil +} + +func stripPEMCertificate(value string) string { + value = strings.ReplaceAll(value, "-----BEGIN CERTIFICATE-----", "") + value = strings.ReplaceAll(value, "-----END CERTIFICATE-----", "") + value = strings.ReplaceAll(value, "\n", "") + value = strings.ReplaceAll(value, "\r", "") + value = strings.ReplaceAll(value, " ", "") + return value +} + +func newKeyDescriptor(use string, cert string) keyDescriptor { + return keyDescriptor{ + Use: use, + KeyInfo: keyInfo{ + Xmlns: xmlDsigNS, + X509Data: x509Data{ + X509Certificate: cert, + }, + }, + } +} + +type entityDescriptor struct { + XMLName xml.Name `xml:"EntityDescriptor"` + Xmlns string `xml:"xmlns,attr"` + EntityID string `xml:"entityID,attr"` + SPSSODescriptor spSSODescriptor `xml:"SPSSODescriptor"` +} + +type spSSODescriptor struct { + ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + AuthnRequestsSigned bool `xml:"AuthnRequestsSigned,attr"` + WantAssertionsSigned bool `xml:"WantAssertionsSigned,attr"` + KeyDescriptors []keyDescriptor `xml:"KeyDescriptor"` + NameIDFormats []string `xml:"NameIDFormat"` + AssertionConsumerServices []assertionConsumerService `xml:"AssertionConsumerService"` +} + +type keyDescriptor struct { + Use string `xml:"use,attr"` + KeyInfo keyInfo `xml:"KeyInfo"` +} + +type keyInfo struct { + Xmlns string `xml:"xmlns,attr"` + X509Data x509Data `xml:"X509Data"` +} + +type x509Data struct { + X509Certificate string `xml:"X509Certificate"` +} + +type assertionConsumerService struct { + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + Index int `xml:"index,attr"` + IsDefault bool `xml:"isDefault,attr"` +} diff --git a/authserver/web/.dockerignore b/authserver/web/.dockerignore new file mode 100644 index 0000000..a7e5733 --- /dev/null +++ b/authserver/web/.dockerignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.DS_Store + diff --git a/authserver/web/Dockerfile b/authserver/web/Dockerfile new file mode 100644 index 0000000..debcaaf --- /dev/null +++ b/authserver/web/Dockerfile @@ -0,0 +1,16 @@ +FROM node:14-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm install --no-audit --no-fund + +COPY . . +RUN npm run build + +FROM nginx:1.29-alpine + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html/auth + +EXPOSE 80 diff --git a/authserver/web/index.html b/authserver/web/index.html new file mode 100644 index 0000000..f469a73 --- /dev/null +++ b/authserver/web/index.html @@ -0,0 +1,12 @@ + + + + + + AuthServer + + +
+ + + diff --git a/authserver/web/nginx.conf b/authserver/web/nginx.conf new file mode 100644 index 0000000..0d567c7 --- /dev/null +++ b/authserver/web/nginx.conf @@ -0,0 +1,53 @@ +server { + listen 80; + server_name _; + + client_max_body_size 10m; + + root /usr/share/nginx/html; + index index.html; + + location = / { + return 302 /auth/; + } + + location = /healthz { + proxy_pass http://authserver-backend:8083; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /readyz { + proxy_pass http://authserver-backend:8083; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /auth/api/ { + proxy_pass http://authserver-backend:8083; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /auth/ { + try_files $uri $uri/ /auth/index.html; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { + expires 7d; + add_header Cache-Control "public, max-age=604800"; + try_files $uri =404; + } +} diff --git a/authserver/web/package-lock.json b/authserver/web/package-lock.json new file mode 100644 index 0000000..6c2df8d --- /dev/null +++ b/authserver/web/package-lock.json @@ -0,0 +1,379 @@ +{ + "name": "authserver-web", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==" + }, + "@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" + }, + "@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "requires": { + "@babel/types": "^7.29.7" + } + }, + "@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "requires": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + } + }, + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "optional": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==" + }, + "@vue/compiler-core": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.17.tgz", + "integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==", + "requires": { + "@babel/parser": "^7.27.5", + "@vue/shared": "3.5.17", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "@vue/compiler-dom": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz", + "integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==", + "requires": { + "@vue/compiler-core": "3.5.17", + "@vue/shared": "3.5.17" + } + }, + "@vue/compiler-sfc": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz", + "integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==", + "requires": { + "@babel/parser": "^7.27.5", + "@vue/compiler-core": "3.5.17", + "@vue/compiler-dom": "3.5.17", + "@vue/compiler-ssr": "3.5.17", + "@vue/shared": "3.5.17", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "@vue/compiler-ssr": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz", + "integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==", + "requires": { + "@vue/compiler-dom": "3.5.17", + "@vue/shared": "3.5.17" + } + }, + "@vue/reactivity": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.17.tgz", + "integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==", + "requires": { + "@vue/shared": "3.5.17" + } + }, + "@vue/runtime-core": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.17.tgz", + "integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==", + "requires": { + "@vue/reactivity": "3.5.17", + "@vue/shared": "3.5.17" + } + }, + "@vue/runtime-dom": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz", + "integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==", + "requires": { + "@vue/reactivity": "3.5.17", + "@vue/runtime-core": "3.5.17", + "@vue/shared": "3.5.17", + "csstype": "^3.1.3" + } + }, + "@vue/server-renderer": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.17.tgz", + "integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==", + "requires": { + "@vue/compiler-ssr": "3.5.17", + "@vue/shared": "3.5.17" + } + }, + "@vue/shared": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz", + "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==" + }, + "csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, + "magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==" + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "postcss": { + "version": "8.5.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.18.tgz", + "integrity": "sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==", + "requires": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "rollup": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", + "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", + "requires": { + "fsevents": "~2.3.2" + } + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "requires": { + "esbuild": "^0.18.10", + "fsevents": "~2.3.2", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + } + }, + "vue": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz", + "integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==", + "requires": { + "@vue/compiler-dom": "3.5.17", + "@vue/compiler-sfc": "3.5.17", + "@vue/runtime-dom": "3.5.17", + "@vue/server-renderer": "3.5.17", + "@vue/shared": "3.5.17" + } + } + } +} diff --git a/authserver/web/package.json b/authserver/web/package.json new file mode 100644 index 0000000..3f7d557 --- /dev/null +++ b/authserver/web/package.json @@ -0,0 +1,20 @@ +{ + "name": "authserver-web", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": ">=14.18.0" + }, + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vite build", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@vitejs/plugin-vue": "4.6.2", + "vite": "4.5.3", + "vue": "3.5.17" + }, + "devDependencies": {} +} diff --git a/authserver/web/src/App.vue b/authserver/web/src/App.vue new file mode 100644 index 0000000..84d7f92 --- /dev/null +++ b/authserver/web/src/App.vue @@ -0,0 +1,211 @@ + + + diff --git a/authserver/web/src/main.js b/authserver/web/src/main.js new file mode 100644 index 0000000..2425c0f --- /dev/null +++ b/authserver/web/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/authserver/web/src/style.css b/authserver/web/src/style.css new file mode 100644 index 0000000..e6af69c --- /dev/null +++ b/authserver/web/src/style.css @@ -0,0 +1,230 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + color: #172033; + background: #f5f7fb; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; +} + +button, +input { + font: inherit; +} + +button { + min-height: 36px; + border: 1px solid #cfd7e6; + border-radius: 6px; + background: #ffffff; + color: #172033; + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +input { + width: 100%; + min-height: 36px; + border: 1px solid #cfd7e6; + border-radius: 6px; + padding: 0 10px; + background: #ffffff; + color: #172033; +} + +h1, +p { + margin: 0; +} + +h1 { + font-size: 22px; + line-height: 1.2; +} + +.login-page { + min-height: 100vh; + display: grid; + place-items: center; + padding: 24px; +} + +.login-panel { + width: min(420px, 100%); + display: grid; + gap: 22px; + border: 1px solid #dbe3ef; + border-radius: 8px; + padding: 28px; + background: #ffffff; + box-shadow: 0 18px 50px rgb(43 57 84 / 10%); +} + +.form { + display: grid; + gap: 10px; +} + +.stack { + gap: 14px; +} + +.form label span { + display: block; + margin-bottom: 6px; + color: #526174; + font-size: 13px; +} + +.primary { + border-color: #1b64d8; + background: #1b64d8; + color: #ffffff; + font-weight: 650; +} + +.secondary { + padding: 0 14px; + border-color: #b8c5d9; + background: #ffffff; +} + +.eyebrow { + margin-bottom: 6px; + color: #526174; + font-size: 12px; + font-weight: 700; + letter-spacing: 0; + text-transform: uppercase; +} + +.message { + margin-top: 4px; + color: #31516f; + font-size: 13px; +} + +.wayen-page { + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr; + background: + linear-gradient(135deg, rgb(22 82 170 / 8%), transparent 42%), + #f5f7fb; +} + +.wayen-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 22px 28px; + border-bottom: 1px solid #dbe3ef; + background: rgb(255 255 255 / 86%); + backdrop-filter: blur(10px); +} + +.wayen-main { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 360px)); + justify-content: center; + align-items: center; + gap: 18px; + padding: 32px 20px; +} + +.wayen-button { + width: min(280px, 78vw); + aspect-ratio: 1; + display: grid; + place-items: center; + align-content: center; + gap: 18px; + border-color: #cfdaf0; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 18px 50px rgb(43 57 84 / 12%); +} + +.wayen-button:hover:not(:disabled), +.wayen-button:focus-visible { + border-color: #1b64d8; + box-shadow: 0 22px 60px rgb(27 100 216 / 18%); + outline: none; +} + +.wayen-icon { + width: 112px; + height: 112px; + display: grid; + place-items: center; + border-radius: 28px; + background: #1b64d8; + color: #ffffff; + font-size: 64px; + font-weight: 800; + line-height: 1; +} + +.wayen-title { + color: #172033; + font-size: 22px; + font-weight: 750; +} + +.user-block { + display: flex; + align-items: center; + gap: 12px; + color: #526174; + font-size: 14px; +} + +.wayen-config { + width: min(360px, 100%); + padding: 22px; + border: 1px solid #dbe3ef; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 18px 50px rgb(43 57 84 / 10%); +} + +.config-title { + color: #172033; + font-size: 16px; + font-weight: 700; +} + +@media (max-width: 560px) { + .wayen-header { + align-items: flex-start; + flex-direction: column; + padding: 18px 20px; + } + + .user-block { + width: 100%; + justify-content: space-between; + } + + .wayen-icon { + width: 92px; + height: 92px; + border-radius: 22px; + font-size: 52px; + } + + .wayen-main { + grid-template-columns: 1fr; + } +} diff --git a/authserver/web/vite.config.js b/authserver/web/vite.config.js new file mode 100644 index 0000000..6cb45ed --- /dev/null +++ b/authserver/web/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + base: '/auth/', + plugins: [vue()], + server: { + port: 5173, + proxy: { + '/auth/api': 'http://127.0.0.1:8083', + '/healthz': 'http://127.0.0.1:8083', + '/readyz': 'http://127.0.0.1:8083', + }, + }, +}) From aa06c05d88407048a1d54c99c4566725d2c18f59 Mon Sep 17 00:00:00 2001 From: mac Date: Mon, 13 Jul 2026 17:20:05 +0800 Subject: [PATCH 02/13] =?UTF-8?q?=E6=89=93=E9=80=9Asso=E5=88=B0wayne?= =?UTF-8?q?=E7=99=BB=E9=99=86=E7=9A=84=E5=85=A8=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- authserver/.gitignore | 2 + authserver/README.md | 6 +- authserver/internal/service/wayen.go | 29 ++++++++++ authserver/web/nginx.conf | 12 ++++ authserver/web/src/App.vue | 87 +++++++++------------------- authserver/web/src/style.css | 31 ---------- 6 files changed, 72 insertions(+), 95 deletions(-) diff --git a/authserver/.gitignore b/authserver/.gitignore index 24fa82a..eca0741 100644 --- a/authserver/.gitignore +++ b/authserver/.gitignore @@ -1,5 +1,7 @@ .cache/ .env +.env.* +!.env.example .DS_Store *.tar bin/ diff --git a/authserver/README.md b/authserver/README.md index 5cba5e2..80d6010 100644 --- a/authserver/README.md +++ b/authserver/README.md @@ -89,7 +89,7 @@ go run ./cmd/server 发布脚本会在本地完成: - 构建后端和前端 Docker 镜像 -- 读取本地 `.env`,生成并应用线上 `ConfigMap`/`Secret` +- 读取本地 `.env.server`,生成并应用线上 `ConfigMap`/`Secret` - 导出 `authserver-images-.tar` - 上传到服务器 - 导入 RKE2 使用的 `containerd` @@ -102,7 +102,7 @@ go run ./cmd/server - 远端目录:`/authserver` - containerd socket:`/run/k3s/containerd/containerd.sock` -发布脚本默认把仓库根目录的 `.env` 当作线上配置源。也就是说,发版前需要先把 `.env` 改成目标环境的实际值;脚本会在发版时重新生成并应用 `authserver-config` 和 `authserver-secret`。 +发布脚本默认把仓库根目录的 `.env.server` 当作服务器配置源;脚本会在发版时重新生成并应用 `authserver-config` 和 `authserver-secret`。本地开发用的 `.env` 不会被默认发布。 脚本默认不会拦截测试环境配置。如果你要对正式环境启用更严格的保护,可以显式打开: @@ -125,7 +125,7 @@ bash scripts/release.sh 1.0.5 如果需要覆盖默认值,可以传环境变量: ```bash -ENV_FILE=.env \ +ENV_FILE=.env.server \ DEPLOY_HOST=218.11.5.223 \ DEPLOY_KEY=/path/to/key.pem \ REMOTE_DIR=/authserver \ diff --git a/authserver/internal/service/wayen.go b/authserver/internal/service/wayen.go index a21f2d3..a80797e 100644 --- a/authserver/internal/service/wayen.go +++ b/authserver/internal/service/wayen.go @@ -53,6 +53,13 @@ func (s *WayenService) Login(email, username string) (*WayenLoginResult, error) if email == "" { return nil, ErrWayenEmailMissing } + if strings.TrimSpace(s.cfg.OAuthRedirectURI) != "" && strings.TrimSpace(s.cfg.WayenTargetURL) != "" { + target, err := s.oauthLoginURL(s.cfg.OAuthRedirectURI, s.cfg.WayenTargetURL) + if err != nil { + return nil, err + } + return &WayenLoginResult{TargetURL: target}, nil + } if strings.TrimSpace(s.cfg.WayenLoginURL) == "" || strings.TrimSpace(s.cfg.WayenTargetURL) == "" { return nil, ErrWayenNotConfigured } @@ -100,6 +107,28 @@ func (s *WayenService) Login(email, username string) (*WayenLoginResult, error) }, nil } +func (s *WayenService) oauthLoginURL(redirectURI, targetURL string) (string, error) { + parsed, err := url.Parse(strings.TrimSpace(redirectURI)) + if err != nil { + return "", err + } + next, err := url.Parse(strings.TrimSpace(targetURL)) + if err != nil { + return "", err + } + if next.Path == "" || next.Path == "/" { + next.Path = "/sign-in" + } + values := next.Query() + values.Set("ref", "oauth") + next.RawQuery = values.Encode() + + query := parsed.Query() + query.Set("next", next.String()) + parsed.RawQuery = query.Encode() + return parsed.String(), nil +} + func (s *WayenService) loginRequest(loginURL, email, password string) (string, io.Reader, string, error) { usernameKey := defaultConfigValue(s.cfg.WayenUsernameKey, "email") passwordKey := defaultConfigValue(s.cfg.WayenPasswordKey, "password") diff --git a/authserver/web/nginx.conf b/authserver/web/nginx.conf index 0d567c7..8b78554 100644 --- a/authserver/web/nginx.conf +++ b/authserver/web/nginx.conf @@ -41,6 +41,18 @@ server { proxy_read_timeout 60s; } + location /auth/oauth/ { + proxy_pass http://authserver-backend:8083; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + location /auth/ { try_files $uri $uri/ /auth/index.html; } diff --git a/authserver/web/src/App.vue b/authserver/web/src/App.vue index 84d7f92..a75ce29 100644 --- a/authserver/web/src/App.vue +++ b/authserver/web/src/App.vue @@ -1,24 +1,16 @@ @@ -168,9 +149,7 @@ onMounted(async () => {

AuthServer

Wayen 登录入口

-

当前仅保留 SSO 登录和 Wayen 单点跳转。

- -

{{ message }}

+

正在跳转到 SSO 登录...

@@ -191,20 +170,6 @@ onMounted(async () => { Wayen -
-

Wayen 凭据

- - - -

{{ message }}

diff --git a/authserver/web/src/style.css b/authserver/web/src/style.css index e6af69c..6c8cffd 100644 --- a/authserver/web/src/style.css +++ b/authserver/web/src/style.css @@ -70,22 +70,6 @@ h1 { box-shadow: 0 18px 50px rgb(43 57 84 / 10%); } -.form { - display: grid; - gap: 10px; -} - -.stack { - gap: 14px; -} - -.form label span { - display: block; - margin-bottom: 6px; - color: #526174; - font-size: 13px; -} - .primary { border-color: #1b64d8; background: #1b64d8; @@ -190,21 +174,6 @@ h1 { font-size: 14px; } -.wayen-config { - width: min(360px, 100%); - padding: 22px; - border: 1px solid #dbe3ef; - border-radius: 8px; - background: #ffffff; - box-shadow: 0 18px 50px rgb(43 57 84 / 10%); -} - -.config-title { - color: #172033; - font-size: 16px; - font-weight: 700; -} - @media (max-width: 560px) { .wayen-header { align-items: flex-start; From 8c7a1ae9eae8f8076f3c57cac3cb0f6ec964977d Mon Sep 17 00:00:00 2001 From: mac Date: Tue, 14 Jul 2026 17:37:08 +0800 Subject: [PATCH 03/13] sync authserver sso integrations --- authserver/.env.example | 6 ++ authserver/README.md | 17 ++++ authserver/internal/auth/jwt.go | 47 ++++++++++- authserver/internal/auth/oidc_key.go | 64 ++++++++++++++ authserver/internal/config/config.go | 64 ++++++++++++++ authserver/internal/handler/clouddm.go | 111 +++++++++++++++++++++++++ authserver/internal/handler/oauth.go | 85 ++++++++++++++++--- authserver/internal/router/router.go | 4 + authserver/internal/service/wayen.go | 2 +- authserver/web/nginx.conf | 25 ++++++ authserver/web/src/App.vue | 77 +++++++++++++---- authserver/web/src/style.css | 31 ++++--- 12 files changed, 490 insertions(+), 43 deletions(-) create mode 100644 authserver/internal/auth/oidc_key.go create mode 100644 authserver/internal/handler/clouddm.go diff --git a/authserver/.env.example b/authserver/.env.example index 827f866..9092794 100644 --- a/authserver/.env.example +++ b/authserver/.env.example @@ -1,6 +1,12 @@ APP_ENV=dev HTTP_ADDR=:8080 PUBLIC_BASE_URL=http://localhost:8080 +OIDC_ISSUER=http://localhost:8080/auth +OIDC_AUTHORIZATION_ENDPOINT=http://localhost:8080/auth/oauth/authorize +OIDC_TOKEN_ENDPOINT=http://localhost:8080/auth/oauth/token +OIDC_USERINFO_ENDPOINT=http://localhost:8080/auth/oauth/userinfo +OIDC_JWKS_URI=http://localhost:8080/auth/oauth/jwks +CLOUDDM_TARGET_URL=http://authserver-nginx/internal/clouddm MYSQL_DSN=auth:auth@tcp(127.0.0.1:3000)/authserver?charset=utf8mb4&parseTime=True&loc=Local AUTO_MIGRATE=true diff --git a/authserver/README.md b/authserver/README.md index 80d6010..37c0c92 100644 --- a/authserver/README.md +++ b/authserver/README.md @@ -161,9 +161,11 @@ bash scripts/restart-authserver.sh nginx | `GET` | `/api/v1/saml/metadata` | SAML SP metadata | | `GET` | `/api/v1/login/internal-sso` | 发起 SAML SSO 登录 | | `POST` | `/api/v1/saml/acs` | SAML ACS 回调,当前仅调试打印 | +| `GET` | `/auth/.well-known/openid-configuration` | OIDC Discovery 配置 | | `GET` | `/auth/oauth/authorize` | OAuth2 Authorization Code 授权入口,供 Wayne 使用 | | `POST` | `/auth/oauth/token` | OAuth2 code 换 access token | | `GET` | `/auth/oauth/userinfo` | OAuth2 bearer token 查询当前用户 | +| `GET` | `/auth/oauth/jwks` | OIDC JWKS 公钥 | ## SAML Metadata @@ -232,6 +234,7 @@ AuthServer 端配置: OAUTH_WAYNE_CLIENT_ID=wayne OAUTH_WAYNE_CLIENT_SECRET=change-this-wayne-client-secret OAUTH_WAYNE_REDIRECT_URI=http://127.0.0.1:8080/login/oauth2/oauth2 +WAYEN_OAUTH_REF=/portal/namespace/1/app OAUTH_CODE_TTL_SECONDS=120 ``` @@ -243,6 +246,19 @@ POST /auth/oauth/token GET /auth/oauth/userinfo ``` +OIDC Discovery 里的 endpoint 默认由 `OIDC_ISSUER` 拼接,也可以按 endpoint 单独覆盖。浏览器需要访问 `OIDC_AUTHORIZATION_ENDPOINT`,后端系统通常访问 `OIDC_TOKEN_ENDPOINT`、`OIDC_USERINFO_ENDPOINT` 和 `OIDC_JWKS_URI`。 + +```env +OIDC_ISSUER=http://auth.example.com/auth +OIDC_AUTHORIZATION_ENDPOINT=http://auth.example.com/auth/oauth/authorize +OIDC_TOKEN_ENDPOINT=http://auth-internal.example.com/auth/oauth/token +OIDC_USERINFO_ENDPOINT=http://auth-internal.example.com/auth/oauth/userinfo +OIDC_JWKS_URI=http://auth-internal.example.com/auth/oauth/jwks +CLOUDDM_TARGET_URL=http://authserver-nginx/internal/clouddm +``` + +`CLOUDDM_TARGET_URL` 用于 AuthServer 后端请求 CloudDM `/requestJumpUrl`。在 k8s 内建议指向 AuthServer nginx 的内部代理路径,由 nginx 转发到 CloudDM Service,并把 `Host` 固定成 CloudDM 公网入口,确保 CloudDM 生成浏览器可访问的 callback。 + Wayne `app.conf` 示例: ```ini @@ -265,6 +281,7 @@ Wayne 会把回调地址拼成: ``` 因此 `OAUTH_WAYNE_REDIRECT_URI` 必须和 Wayne 实际回调地址完全一致。浏览器访问 Wayne OAuth 登录入口后,如果 AuthServer 还没有登录态,会先跳内部 SAML;SAML 成功后再回到 OAuth authorize,签发 code 给 Wayne。 +`WAYEN_OAUTH_REF` 是 AuthServer 发起 Wayne 登录时写入 Wayne `next` 参数的登录完成页,默认 `/portal/namespace/1/app`,对应 Wayne `DemoNamespaceId = 1` 的默认 namespace。不要配置成 `oauth` 或 `/oauth`,否则 Wayne 回调会把它当成前端路由跳到 `/oauth`。 管理员可查看当前 SAML metadata 配置: diff --git a/authserver/internal/auth/jwt.go b/authserver/internal/auth/jwt.go index ef9e786..9a6ea4f 100644 --- a/authserver/internal/auth/jwt.go +++ b/authserver/internal/auth/jwt.go @@ -1,6 +1,7 @@ package auth import ( + "crypto/rsa" "errors" "time" @@ -19,6 +20,18 @@ type OAuthCodeClaims struct { UserID uint64 `json:"uid"` ClientID string `json:"client_id"` RedirectURI string `json:"redirect_uri"` + Scope string `json:"scope"` + Nonce string `json:"nonce,omitempty"` + jwt.RegisteredClaims +} + +type IDTokenClaims struct { + UserID uint64 `json:"uid"` + Username string `json:"preferred_username"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + Name string `json:"name"` + Nonce string `json:"nonce,omitempty"` jwt.RegisteredClaims } @@ -45,7 +58,7 @@ func Sign(secret, issuer string, ttl time.Duration, userID uint64, username, ema return signed, tokenID, expiresAt, err } -func SignOAuthCode(secret, issuer string, ttl time.Duration, userID uint64, clientID, redirectURI string) (string, string, time.Time, error) { +func SignOAuthCode(secret, issuer string, ttl time.Duration, userID uint64, clientID, redirectURI, scope, nonce string) (string, string, time.Time, error) { now := time.Now() expiresAt := now.Add(ttl) codeID := randomID() @@ -53,6 +66,8 @@ func SignOAuthCode(secret, issuer string, ttl time.Duration, userID uint64, clie UserID: userID, ClientID: clientID, RedirectURI: redirectURI, + Scope: scope, + Nonce: nonce, RegisteredClaims: jwt.RegisteredClaims{ ID: codeID, Issuer: issuer, @@ -67,6 +82,36 @@ func SignOAuthCode(secret, issuer string, ttl time.Duration, userID uint64, clie return signed, codeID, expiresAt, err } +func SignIDToken(privateKey *rsa.PrivateKey, keyID, issuer, clientID string, ttl time.Duration, userID uint64, username, email, name, nonce string) (string, time.Time, error) { + now := time.Now() + expiresAt := now.Add(ttl) + subject := email + if subject == "" { + subject = username + } + claims := IDTokenClaims{ + UserID: userID, + Username: username, + Email: email, + EmailVerified: email != "", + Name: name, + Nonce: nonce, + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: issuer, + Subject: subject, + Audience: []string{clientID}, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(expiresAt), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + if keyID != "" { + token.Header["kid"] = keyID + } + signed, err := token.SignedString(privateKey) + return signed, expiresAt, err +} + func Parse(secret string, tokenValue string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenValue, &Claims{}, func(token *jwt.Token) (any, error) { if token.Method != jwt.SigningMethodHS256 { diff --git a/authserver/internal/auth/oidc_key.go b/authserver/internal/auth/oidc_key.go new file mode 100644 index 0000000..fde8bfc --- /dev/null +++ b/authserver/internal/auth/oidc_key.go @@ -0,0 +1,64 @@ +package auth + +import ( + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "math/big" + "os" +) + +type JWK struct { + KeyType string `json:"kty"` + Use string `json:"use"` + KeyID string `json:"kid"` + Algorithm string `json:"alg"` + Modulus string `json:"n"` + Exponent string `json:"e"` +} + +func LoadRSAPrivateKey(path string) (*rsa.PrivateKey, error) { + if path == "" { + return nil, errors.New("rsa private key file is required") + } + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + block, _ := pem.Decode(data) + if block == nil { + return nil, errors.New("rsa private key file has no PEM block") + } + if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { + return key, nil + } + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + key, ok := parsed.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("private key is not an RSA private key") + } + return key, nil +} + +func PublicJWKFromKey(key *rsa.PrivateKey) JWK { + publicKey := key.PublicKey + return JWK{ + KeyType: "RSA", + Use: "sig", + KeyID: RSAKeyID(&publicKey), + Algorithm: "RS256", + Modulus: base64.RawURLEncoding.EncodeToString(publicKey.N.Bytes()), + Exponent: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(publicKey.E)).Bytes()), + } +} + +func RSAKeyID(publicKey *rsa.PublicKey) string { + hash := sha256.Sum256(publicKey.N.Bytes()) + return base64.RawURLEncoding.EncodeToString(hash[:8]) +} diff --git a/authserver/internal/config/config.go b/authserver/internal/config/config.go index d5a63fa..9e58da7 100644 --- a/authserver/internal/config/config.go +++ b/authserver/internal/config/config.go @@ -7,6 +7,12 @@ import ( "time" ) +type OAuthClient struct { + ID string + Secret string + RedirectURIs []string +} + type Config struct { AppEnv string HTTPAddr string @@ -27,10 +33,20 @@ type Config struct { WayenPasswordKey string WayenLoginFormat string WayenLoginValue string + WayenOAuthRef string OAuthClientID string OAuthClientSecret string OAuthRedirectURI string OAuthCodeTTLSeconds int + OIDCIssuer string + OIDCAuthorizeURL string + OIDCTokenURL string + OIDCUserInfoURL string + OIDCJWKSURL string + CloudDMClientID string + CloudDMClientSecret string + CloudDMRedirectURI string + CloudDMTargetURL string } func Load() Config { @@ -39,6 +55,8 @@ func Load() Config { publicBaseURL := env("PUBLIC_BASE_URL", defaultPublicBaseURL(httpAddr)) samlEntityID := env("SAML_ENTITY_ID", strings.TrimRight(publicBaseURL, "/")+"/auth/api/v1/saml/metadata") samlACSURL := env("SAML_ACS_URL", strings.TrimRight(publicBaseURL, "/")+"/auth/api/v1/saml/acs") + oidcIssuer := env("OIDC_ISSUER", strings.TrimRight(publicBaseURL, "/")+"/auth") + oidcIssuer = strings.TrimRight(oidcIssuer, "/") return Config{ AppEnv: env("APP_ENV", "dev"), @@ -60,10 +78,20 @@ func Load() Config { WayenPasswordKey: env("WAYEN_PASSWORD_KEY", "password"), WayenLoginFormat: env("WAYEN_LOGIN_FORMAT", "form"), WayenLoginValue: env("WAYEN_LOGIN_VALUE", "email"), + WayenOAuthRef: env("WAYEN_OAUTH_REF", "/portal/namespace/1/app"), OAuthClientID: env("OAUTH_WAYNE_CLIENT_ID", "wayne"), OAuthClientSecret: env("OAUTH_WAYNE_CLIENT_SECRET", "wayne-secret"), OAuthRedirectURI: env("OAUTH_WAYNE_REDIRECT_URI", ""), OAuthCodeTTLSeconds: envInt("OAUTH_CODE_TTL_SECONDS", 120), + OIDCIssuer: oidcIssuer, + OIDCAuthorizeURL: trimURL(env("OIDC_AUTHORIZATION_ENDPOINT", oidcIssuer+"/oauth/authorize")), + OIDCTokenURL: trimURL(env("OIDC_TOKEN_ENDPOINT", oidcIssuer+"/oauth/token")), + OIDCUserInfoURL: trimURL(env("OIDC_USERINFO_ENDPOINT", oidcIssuer+"/oauth/userinfo")), + OIDCJWKSURL: trimURL(env("OIDC_JWKS_URI", oidcIssuer+"/oauth/jwks")), + CloudDMClientID: env("OIDC_CLOUDDM_CLIENT_ID", "clouddm"), + CloudDMClientSecret: env("OIDC_CLOUDDM_CLIENT_SECRET", ""), + CloudDMRedirectURI: env("OIDC_CLOUDDM_REDIRECT_URI", ""), + CloudDMTargetURL: env("CLOUDDM_TARGET_URL", ""), } } @@ -99,6 +127,42 @@ func (c Config) OAuthCodeTTL() time.Duration { return time.Duration(c.OAuthCodeTTLSeconds) * time.Second } +func (c Config) OAuthClients() map[string]OAuthClient { + clients := make(map[string]OAuthClient) + addOAuthClient(clients, c.OAuthClientID, c.OAuthClientSecret, c.OAuthRedirectURI) + addOAuthClient(clients, c.CloudDMClientID, c.CloudDMClientSecret, c.CloudDMRedirectURI) + return clients +} + +func addOAuthClient(clients map[string]OAuthClient, id, secret, redirectURIs string) { + id = strings.TrimSpace(id) + secret = strings.TrimSpace(secret) + if id == "" || secret == "" { + return + } + clients[id] = OAuthClient{ + ID: id, + Secret: secret, + RedirectURIs: splitCSV(redirectURIs), + } +} + +func splitCSV(value string) []string { + parts := strings.Split(value, ",") + items := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + items = append(items, part) + } + } + return items +} + +func trimURL(value string) string { + return strings.TrimRight(strings.TrimSpace(value), "/") +} + func env(key, fallback string) string { value := os.Getenv(key) if value == "" { diff --git a/authserver/internal/handler/clouddm.go b/authserver/internal/handler/clouddm.go new file mode 100644 index 0000000..904e6b8 --- /dev/null +++ b/authserver/internal/handler/clouddm.go @@ -0,0 +1,111 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "authserver/internal/config" + "authserver/internal/service" + + "github.com/gin-gonic/gin" +) + +type CloudDMHandler struct { + cfg config.Config + audit *service.AuditService +} + +func NewCloudDMHandler(cfg config.Config, audit *service.AuditService) *CloudDMHandler { + return &CloudDMHandler{cfg: cfg, audit: audit} +} + +func (h *CloudDMHandler) Login(c *gin.Context) { + claims, ok := CurrentClaims(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) + return + } + + targetURL := strings.TrimSpace(h.cfg.CloudDMTargetURL) + if targetURL == "" { + h.writeAudit(c, claims.UserID, claims.Username, "deny", "clouddm target url is not configured") + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "clouddm target url is not configured"}) + return + } + + jumpURL, err := h.loginJumpURL(targetURL) + if err != nil { + h.writeAudit(c, claims.UserID, claims.Username, "deny", err.Error()) + c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) + return + } + + h.writeAudit(c, claims.UserID, claims.Username, "allow", "") + if strings.Contains(c.GetHeader("Accept"), "application/json") { + c.JSON(http.StatusOK, gin.H{"target_url": jumpURL}) + return + } + c.Redirect(http.StatusFound, jumpURL) +} + +func (h *CloudDMHandler) loginJumpURL(targetURL string) (string, error) { + requestURL := strings.TrimRight(targetURL, "/") + "/requestJumpUrl" + body, _ := json.Marshal(gin.H{"type": "OIDC"}) + req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(body)) + if err != nil { + return "", err + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", fmt.Errorf("clouddm requestJumpUrl failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw))) + } + + var result struct { + Success bool `json:"success"` + Data string `json:"data"` + Msg string `json:"msg"` + MsgContent string `json:"msgContent"` + } + if err := json.Unmarshal(raw, &result); err != nil { + return "", err + } + if !result.Success || strings.TrimSpace(result.Data) == "" { + reason := strings.TrimSpace(result.MsgContent) + if reason == "" { + reason = strings.TrimSpace(result.Msg) + } + if reason == "" { + reason = "empty clouddm jump url" + } + return "", fmt.Errorf("clouddm requestJumpUrl failed: %s", reason) + } + return result.Data, nil +} + +func (h *CloudDMHandler) writeAudit(c *gin.Context, userID uint64, username, decision, reason string) { + h.audit.Write(service.AuditEntry{ + ActorUserID: userID, + ActorUsername: username, + ClientIP: c.ClientIP(), + UserAgent: c.Request.UserAgent(), + Action: "clouddm.login", + ResourceType: "clouddm", + Decision: decision, + Reason: reason, + }) +} diff --git a/authserver/internal/handler/oauth.go b/authserver/internal/handler/oauth.go index db61b34..cffa577 100644 --- a/authserver/internal/handler/oauth.go +++ b/authserver/internal/handler/oauth.go @@ -40,12 +40,15 @@ func (h *OAuthHandler) Authorize(c *gin.Context) { redirectURI := strings.TrimSpace(c.Query("redirect_uri")) responseType := strings.TrimSpace(c.Query("response_type")) state := c.Query("state") + scope := strings.TrimSpace(c.Query("scope")) + nonce := strings.TrimSpace(c.Query("nonce")) if responseType != "code" { c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported response_type"}) return } - if !h.validClientRedirect(clientID, redirectURI) { + client, ok := h.client(clientID) + if !ok || !validClientRedirect(client, redirectURI) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid client_id or redirect_uri"}) return } @@ -57,7 +60,7 @@ func (h *OAuthHandler) Authorize(c *gin.Context) { return } - code, codeID, expiresAt, err := auth.SignOAuthCode(h.cfg.JWTSecret, h.cfg.JWTIssuer, h.cfg.OAuthCodeTTL(), user.ID, clientID, redirectURI) + code, codeID, expiresAt, err := auth.SignOAuthCode(h.cfg.JWTSecret, h.cfg.JWTIssuer, h.cfg.OAuthCodeTTL(), user.ID, clientID, redirectURI, scope, nonce) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -119,7 +122,8 @@ func (h *OAuthHandler) Token(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported grant_type"}) return } - if clientID != h.cfg.OAuthClientID || clientSecret != h.cfg.OAuthClientSecret { + client, ok := h.client(clientID) + if !ok || clientSecret != client.Secret { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid client credentials"}) return } @@ -129,12 +133,13 @@ func (h *OAuthHandler) Token(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid code"}) return } - if codeClaims.ClientID != clientID || codeClaims.RedirectURI != redirectURI || !h.validClientRedirect(clientID, redirectURI) { + if codeClaims.ClientID != clientID || codeClaims.RedirectURI != redirectURI || !validClientRedirect(client, redirectURI) { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid code"}) return } var token string + var idToken string var expiresAt time.Time var user model.User now := time.Now() @@ -152,12 +157,25 @@ func (h *OAuthHandler) Token(c *gin.Context) { return service.ErrUserDisabled } + display := strings.TrimSpace(user.DisplayName) + if display == "" { + display = user.Username + } tokenID := "" var err error token, tokenID, expiresAt, err = auth.Sign(h.cfg.JWTSecret, h.cfg.JWTIssuer, h.cfg.JWTTTL(), user.ID, user.Username, user.Email, user.IsAdmin) if err != nil { return err } + privateKey, err := auth.LoadRSAPrivateKey(h.cfg.SAMLSPKey) + if err != nil { + return err + } + keyID := auth.RSAKeyID(&privateKey.PublicKey) + idToken, _, err = auth.SignIDToken(privateKey, keyID, h.cfg.OIDCIssuer, clientID, h.cfg.JWTTTL(), user.ID, user.Username, user.Email, display, codeClaims.Nonce) + if err != nil { + return err + } if err := tx.Model(&codeRecord).Updates(map[string]any{ "revoked": true, "revoked_at": &now, @@ -195,11 +213,41 @@ func (h *OAuthHandler) Token(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "access_token": token, + "id_token": idToken, "token_type": "Bearer", "expires_in": int(time.Until(expiresAt).Seconds()), }) } +func (h *OAuthHandler) Discovery(c *gin.Context) { + issuer := strings.TrimRight(h.cfg.OIDCIssuer, "/") + c.JSON(http.StatusOK, gin.H{ + "issuer": issuer, + "authorization_endpoint": h.cfg.OIDCAuthorizeURL, + "token_endpoint": h.cfg.OIDCTokenURL, + "userinfo_endpoint": h.cfg.OIDCUserInfoURL, + "jwks_uri": h.cfg.OIDCJWKSURL, + "response_types_supported": []string{"code"}, + "grant_types_supported": []string{"authorization_code"}, + "subject_types_supported": []string{"public"}, + "id_token_signing_alg_values_supported": []string{"RS256"}, + "scopes_supported": []string{"openid", "profile", "email"}, + "claims_supported": []string{"sub", "name", "preferred_username", "email", "email_verified"}, + "token_endpoint_auth_methods_supported": []string{"client_secret_basic", "client_secret_post"}, + }) +} + +func (h *OAuthHandler) JWKS(c *gin.Context) { + privateKey, err := auth.LoadRSAPrivateKey(h.cfg.SAMLSPKey) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{ + "keys": []auth.JWK{auth.PublicJWKFromKey(privateKey)}, + }) +} + func (h *OAuthHandler) UserInfo(c *gin.Context) { claims, err := bearerClaims(h.cfg, c.GetHeader("Authorization")) if err != nil { @@ -224,22 +272,35 @@ func (h *OAuthHandler) UserInfo(c *gin.Context) { display = user.Username } c.JSON(http.StatusOK, gin.H{ - "name": user.Username, - "email": user.Email, - "display": display, + "sub": user.Email, + "name": user.Username, + "preferred_username": user.Username, + "email": user.Email, + "email_verified": user.Email != "", + "display": display, }) } func (h *OAuthHandler) oauthConfigured() bool { - return h.cfg.OAuthClientID != "" && h.cfg.OAuthClientSecret != "" + return len(h.cfg.OAuthClients()) > 0 } -func (h *OAuthHandler) validClientRedirect(clientID, redirectURI string) bool { - if clientID == "" || clientID != h.cfg.OAuthClientID || redirectURI == "" { +func (h *OAuthHandler) client(clientID string) (config.OAuthClient, bool) { + client, ok := h.cfg.OAuthClients()[strings.TrimSpace(clientID)] + return client, ok +} + +func validClientRedirect(client config.OAuthClient, redirectURI string) bool { + if redirectURI == "" { return false } - if h.cfg.OAuthRedirectURI != "" { - return redirectURI == h.cfg.OAuthRedirectURI + if len(client.RedirectURIs) > 0 { + for _, allowed := range client.RedirectURIs { + if redirectURI == allowed { + return true + } + } + return false } parsed, err := url.Parse(redirectURI) return err == nil && parsed.IsAbs() && (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host != "" diff --git a/authserver/internal/router/router.go b/authserver/internal/router/router.go index 01556c4..3390113 100644 --- a/authserver/internal/router/router.go +++ b/authserver/internal/router/router.go @@ -29,13 +29,16 @@ func New(deps Dependencies) *gin.Engine { healthHandler := handler.NewHealthHandler(deps.DB) userHandler := handler.NewUserHandler() wayenHandler := handler.NewWayenHandler(deps.DB, wayenService, auditService) + clouddmHandler := handler.NewCloudDMHandler(deps.Config, auditService) samlHandler := handler.NewSAMLHandler(deps.Config, authService) oauthHandler := handler.NewOAuthHandler(deps.Config, deps.DB, auditService) r.GET("/healthz", healthHandler.Healthz) r.GET("/readyz", healthHandler.Readyz) + r.GET("/auth/.well-known/openid-configuration", oauthHandler.Discovery) r.GET("/auth/oauth/authorize", oauthHandler.Authorize) r.POST("/auth/oauth/token", oauthHandler.Token) + r.GET("/auth/oauth/jwks", oauthHandler.JWKS) r.GET("/auth/oauth/userinfo", oauthHandler.UserInfo) v1 := r.Group("/auth/api/v1") @@ -50,6 +53,7 @@ func New(deps Dependencies) *gin.Engine { protected.GET("/wayen/login", wayenHandler.Login) protected.GET("/wayen/credential", wayenHandler.GetCredential) protected.PUT("/wayen/credential", wayenHandler.SaveCredential) + protected.GET("/clouddm/login", clouddmHandler.Login) } return r diff --git a/authserver/internal/service/wayen.go b/authserver/internal/service/wayen.go index a80797e..f370ab5 100644 --- a/authserver/internal/service/wayen.go +++ b/authserver/internal/service/wayen.go @@ -120,7 +120,7 @@ func (s *WayenService) oauthLoginURL(redirectURI, targetURL string) (string, err next.Path = "/sign-in" } values := next.Query() - values.Set("ref", "oauth") + values.Set("ref", defaultConfigValue(s.cfg.WayenOAuthRef, "/portal/namespace/1/app")) next.RawQuery = values.Encode() query := parsed.Query() diff --git a/authserver/web/nginx.conf b/authserver/web/nginx.conf index 8b78554..caaae48 100644 --- a/authserver/web/nginx.conf +++ b/authserver/web/nginx.conf @@ -53,6 +53,31 @@ server { proxy_read_timeout 60s; } + location /auth/.well-known/ { + proxy_pass http://authserver-backend:8083; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /internal/clouddm/ { + proxy_pass http://open-cdm.db.svc.cluster.local:8222/; + proxy_http_version 1.1; + proxy_set_header Host 218.11.5.223:30009; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Host 218.11.5.223:30009; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto http; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + location /auth/ { try_files $uri $uri/ /auth/index.html; } diff --git a/authserver/web/src/App.vue b/authserver/web/src/App.vue index a75ce29..be8f366 100644 --- a/authserver/web/src/App.vue +++ b/authserver/web/src/App.vue @@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue' const TOKEN_KEY = 'authserver_token' const SSO_LOGIN_PATH = '/auth/api/v1/login/internal-sso' const WAYEN_LOGIN_PATH = '/auth/api/v1/wayen/login' +const CLOUDDM_LOGIN_PATH = '/auth/api/v1/clouddm/login' const token = ref(localStorage.getItem(TOKEN_KEY) || '') const currentUser = ref(null) @@ -11,6 +12,7 @@ const message = ref('') const loading = ref(false) const redirectingToSSO = ref(false) const wayenLoading = ref(false) +const clouddmLoading = ref(false) const isAuthed = computed(() => Boolean(token.value)) const currentUserLabel = computed(() => { @@ -26,20 +28,24 @@ function clearAuth() { localStorage.removeItem(TOKEN_KEY) } -function relayState(openWayen = false) { +function relayState(openApp = '') { const url = new URL(window.location.href) - if (openWayen) { + url.searchParams.delete('open_wayen') + url.searchParams.delete('open_app') + if (openApp === 'wayen') { url.searchParams.set('open_wayen', '1') + } else if (openApp) { + url.searchParams.set('open_app', openApp) } return `${url.pathname}${url.search}${url.hash}` || '/' } -function ssoLogin({ openWayen = false } = {}) { +function ssoLogin({ openApp = '' } = {}) { if (redirectingToSSO.value) { return } redirectingToSSO.value = true - window.location.assign(`${SSO_LOGIN_PATH}?relay_state=${encodeURIComponent(relayState(openWayen))}`) + window.location.assign(`${SSO_LOGIN_PATH}?relay_state=${encodeURIComponent(relayState(openApp))}`) } async function api(path, options = {}) { @@ -55,7 +61,7 @@ async function api(path, options = {}) { if (!response.ok) { if (response.status === 401) { clearAuth() - ssoLogin({ openWayen: true }) + ssoLogin() } const error = new Error(data.error || `HTTP ${response.status}`) error.status = response.status @@ -103,7 +109,7 @@ async function openWayen() { if (!response.ok) { if (response.status === 401) { clearAuth() - ssoLogin({ openWayen: true }) + ssoLogin({ openApp: 'wayen' }) } throw new Error(data.error || `HTTP ${response.status}`) } @@ -118,26 +124,59 @@ async function openWayen() { } } +async function openCloudDM() { + clouddmLoading.value = true + message.value = '' + try { + const response = await fetch(CLOUDDM_LOGIN_PATH, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token.value}`, + }, + credentials: 'include', + }) + const data = await response.json().catch(() => ({})) + if (!response.ok) { + if (response.status === 401) { + clearAuth() + ssoLogin({ openApp: 'clouddm' }) + } + throw new Error(data.error || `HTTP ${response.status}`) + } + if (!data.target_url) { + throw new Error('CloudDM 跳转地址为空') + } + window.location.assign(data.target_url) + } catch (error) { + message.value = error.message + } finally { + clouddmLoading.value = false + } +} + onMounted(async () => { const url = new URL(window.location.href) const ssoToken = url.searchParams.get('sso_token') - const shouldOpenWayen = url.searchParams.get('open_wayen') === '1' + const openApp = url.searchParams.get('open_wayen') === '1' ? 'wayen' : url.searchParams.get('open_app') if (ssoToken) { token.value = ssoToken localStorage.setItem(TOKEN_KEY, ssoToken) url.searchParams.delete('sso_token') url.searchParams.delete('open_wayen') + url.searchParams.delete('open_app') window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`) } if (!token.value) { - ssoLogin({ openWayen: true }) + ssoLogin() return } await run(async () => { await loadMe() }, '已就绪') - if (shouldOpenWayen) { + if (openApp === 'wayen') { await openWayen() + } else if (openApp === 'clouddm') { + await openCloudDM() } }) @@ -147,17 +186,17 @@ onMounted(async () => { -
-
+
+

AuthServer

-

Wayen

+

应用入口

{{ currentUserLabel }} @@ -165,10 +204,14 @@ onMounted(async () => {
-
- +

{{ message }}

diff --git a/authserver/web/src/style.css b/authserver/web/src/style.css index 6c8cffd..9283195 100644 --- a/authserver/web/src/style.css +++ b/authserver/web/src/style.css @@ -98,7 +98,7 @@ h1 { font-size: 13px; } -.wayen-page { +.app-page { min-height: 100vh; display: grid; grid-template-rows: auto 1fr; @@ -107,7 +107,7 @@ h1 { #f5f7fb; } -.wayen-header { +.app-header { display: flex; align-items: center; justify-content: space-between; @@ -118,7 +118,7 @@ h1 { backdrop-filter: blur(10px); } -.wayen-main { +.app-main { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 360px)); justify-content: center; @@ -127,7 +127,7 @@ h1 { padding: 32px 20px; } -.wayen-button { +.app-button { width: min(280px, 78vw); aspect-ratio: 1; display: grid; @@ -140,27 +140,34 @@ h1 { box-shadow: 0 18px 50px rgb(43 57 84 / 12%); } -.wayen-button:hover:not(:disabled), -.wayen-button:focus-visible { +.app-button:hover:not(:disabled), +.app-button:focus-visible { border-color: #1b64d8; box-shadow: 0 22px 60px rgb(27 100 216 / 18%); outline: none; } -.wayen-icon { +.app-icon { width: 112px; height: 112px; display: grid; place-items: center; border-radius: 28px; - background: #1b64d8; color: #ffffff; font-size: 64px; font-weight: 800; line-height: 1; } -.wayen-title { +.wayen-icon { + background: #1b64d8; +} + +.clouddm-icon { + background: #0f7b6c; +} + +.app-title { color: #172033; font-size: 22px; font-weight: 750; @@ -175,7 +182,7 @@ h1 { } @media (max-width: 560px) { - .wayen-header { + .app-header { align-items: flex-start; flex-direction: column; padding: 18px 20px; @@ -186,14 +193,14 @@ h1 { justify-content: space-between; } - .wayen-icon { + .app-icon { width: 92px; height: 92px; border-radius: 22px; font-size: 52px; } - .wayen-main { + .app-main { grid-template-columns: 1fr; } } From afe6c409013a1e8881f2532b0b1b1640362d206c Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 11:38:27 +0800 Subject: [PATCH 04/13] fix(auth): move nginx config to k8s --- authserver/.gitignore | 2 +- authserver/cmd/server/main.go | 37 +++ authserver/internal/handler/saml.go | 8 +- authserver/k8s/nginx.yaml | 148 +++++++++++ authserver/web/.dockerignore | 4 - authserver/web/Dockerfile | 16 -- authserver/web/index.html | 12 - authserver/web/nginx.conf | 90 ------- authserver/web/package-lock.json | 379 ---------------------------- authserver/web/package.json | 20 -- authserver/web/src/App.vue | 219 ---------------- authserver/web/src/main.js | 5 - authserver/web/src/style.css | 206 --------------- authserver/web/vite.config.js | 15 -- 14 files changed, 190 insertions(+), 971 deletions(-) create mode 100644 authserver/cmd/server/main.go create mode 100644 authserver/k8s/nginx.yaml delete mode 100644 authserver/web/.dockerignore delete mode 100644 authserver/web/Dockerfile delete mode 100644 authserver/web/index.html delete mode 100644 authserver/web/nginx.conf delete mode 100644 authserver/web/package-lock.json delete mode 100644 authserver/web/package.json delete mode 100644 authserver/web/src/App.vue delete mode 100644 authserver/web/src/main.js delete mode 100644 authserver/web/src/style.css delete mode 100644 authserver/web/vite.config.js diff --git a/authserver/.gitignore b/authserver/.gitignore index eca0741..6097006 100644 --- a/authserver/.gitignore +++ b/authserver/.gitignore @@ -5,7 +5,7 @@ .DS_Store *.tar bin/ -server +/server web/node_modules/ web/dist/ certs/ diff --git a/authserver/cmd/server/main.go b/authserver/cmd/server/main.go new file mode 100644 index 0000000..7ad3580 --- /dev/null +++ b/authserver/cmd/server/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "log" + + "authserver/internal/config" + "authserver/internal/database" + "authserver/internal/router" +) + +func main() { + cfg := config.Load() + + db, err := database.Open(cfg.MySQLDSN) + if err != nil { + log.Fatalf("open database: %v", err) + } + + if cfg.AutoMigrate { + if err := database.AutoMigrate(db); err != nil { + log.Fatalf("auto migrate: %v", err) + } + } + if err := database.SeedDefaults(db, cfg); err != nil { + log.Fatalf("seed defaults: %v", err) + } + + r := router.New(router.Dependencies{ + Config: cfg, + DB: db, + }) + + log.Printf("authserver listening on %s", cfg.HTTPAddr) + if err := r.Run(cfg.HTTPAddr); err != nil { + log.Fatalf("run server: %v", err) + } +} diff --git a/authserver/internal/handler/saml.go b/authserver/internal/handler/saml.go index a90818d..0ca7f23 100644 --- a/authserver/internal/handler/saml.go +++ b/authserver/internal/handler/saml.go @@ -97,15 +97,15 @@ func (h *SAMLHandler) ACS(c *gin.Context) { func ssoRedirectURL(relayState, token string) string { target := strings.TrimSpace(relayState) - if target == "" || !strings.HasPrefix(target, "/auth/") { - target = "/auth/" + if target == "" || !strings.HasPrefix(target, "/") { + target = "/" } if strings.HasPrefix(target, "//") { - target = "/auth/" + target = "/" } parsed, err := url.Parse(target) if err != nil || parsed.IsAbs() { - parsed = &url.URL{Path: "/auth/"} + parsed = &url.URL{Path: "/"} } values := parsed.Query() values.Set("sso_token", token) diff --git a/authserver/k8s/nginx.yaml b/authserver/k8s/nginx.yaml new file mode 100644 index 0000000..fae0081 --- /dev/null +++ b/authserver/k8s/nginx.yaml @@ -0,0 +1,148 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: authserver-nginx-conf + namespace: authserver +data: + default.conf.template: | + server { + listen 80; + server_name _; + + client_max_body_size 10m; + + location = /healthz { + proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /readyz { + proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /auth/api/ { + proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /auth/oauth/ { + proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /auth/.well-known/ { + proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /internal/clouddm/ { + proxy_pass ${CLOUDDM_UPSTREAM}/; + proxy_http_version 1.1; + proxy_set_header Host ${CLOUDDM_PUBLIC_HOST}; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Host ${CLOUDDM_PUBLIC_HOST}; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto http; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location / { + return 404; + } + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authserver-nginx + namespace: authserver +spec: + replicas: 1 + selector: + matchLabels: + app: authserver-nginx + template: + metadata: + labels: + app: authserver-nginx + spec: + containers: + - name: nginx + image: nginx:1.29-alpine + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + env: + - name: AUTHSERVER_BACKEND_UPSTREAM + value: http://authserver-backend:8083 + - name: CLOUDDM_UPSTREAM + value: http://open-cdm.db.svc.cluster.local:8222 + - name: CLOUDDM_PUBLIC_HOST + value: 218.11.5.223:30009 + volumeMounts: + - name: nginx-conf + mountPath: /etc/nginx/templates/default.conf.template + subPath: default.conf.template + readinessProbe: + httpGet: + path: /readyz + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: 80 + initialDelaySeconds: 10 + periodSeconds: 20 + volumes: + - name: nginx-conf + configMap: + name: authserver-nginx-conf +--- +apiVersion: v1 +kind: Service +metadata: + name: authserver-nginx + namespace: authserver +spec: + type: NodePort + selector: + app: authserver-nginx + ports: + - name: http + port: 80 + targetPort: 80 + nodePort: 30080 diff --git a/authserver/web/.dockerignore b/authserver/web/.dockerignore deleted file mode 100644 index a7e5733..0000000 --- a/authserver/web/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -dist/ -.DS_Store - diff --git a/authserver/web/Dockerfile b/authserver/web/Dockerfile deleted file mode 100644 index debcaaf..0000000 --- a/authserver/web/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM node:14-alpine AS builder - -WORKDIR /app - -COPY package*.json ./ -RUN npm install --no-audit --no-fund - -COPY . . -RUN npm run build - -FROM nginx:1.29-alpine - -COPY nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=builder /app/dist /usr/share/nginx/html/auth - -EXPOSE 80 diff --git a/authserver/web/index.html b/authserver/web/index.html deleted file mode 100644 index f469a73..0000000 --- a/authserver/web/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - AuthServer - - -
- - - diff --git a/authserver/web/nginx.conf b/authserver/web/nginx.conf deleted file mode 100644 index caaae48..0000000 --- a/authserver/web/nginx.conf +++ /dev/null @@ -1,90 +0,0 @@ -server { - listen 80; - server_name _; - - client_max_body_size 10m; - - root /usr/share/nginx/html; - index index.html; - - location = / { - return 302 /auth/; - } - - location = /healthz { - proxy_pass http://authserver-backend:8083; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location = /readyz { - proxy_pass http://authserver-backend:8083; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /auth/api/ { - proxy_pass http://authserver-backend:8083; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 5s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - location /auth/oauth/ { - proxy_pass http://authserver-backend:8083; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 5s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - location /auth/.well-known/ { - proxy_pass http://authserver-backend:8083; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 5s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - location /internal/clouddm/ { - proxy_pass http://open-cdm.db.svc.cluster.local:8222/; - proxy_http_version 1.1; - proxy_set_header Host 218.11.5.223:30009; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Host 218.11.5.223:30009; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto http; - proxy_connect_timeout 5s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - location /auth/ { - try_files $uri $uri/ /auth/index.html; - } - - location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { - expires 7d; - add_header Cache-Control "public, max-age=604800"; - try_files $uri =404; - } -} diff --git a/authserver/web/package-lock.json b/authserver/web/package-lock.json deleted file mode 100644 index 6c2df8d..0000000 --- a/authserver/web/package-lock.json +++ /dev/null @@ -1,379 +0,0 @@ -{ - "name": "authserver-web", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==" - }, - "@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" - }, - "@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "requires": { - "@babel/types": "^7.29.7" - } - }, - "@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "requires": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - } - }, - "@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "optional": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" - }, - "@vitejs/plugin-vue": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", - "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==" - }, - "@vue/compiler-core": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.17.tgz", - "integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==", - "requires": { - "@babel/parser": "^7.27.5", - "@vue/shared": "3.5.17", - "entities": "^4.5.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "@vue/compiler-dom": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz", - "integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==", - "requires": { - "@vue/compiler-core": "3.5.17", - "@vue/shared": "3.5.17" - } - }, - "@vue/compiler-sfc": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz", - "integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==", - "requires": { - "@babel/parser": "^7.27.5", - "@vue/compiler-core": "3.5.17", - "@vue/compiler-dom": "3.5.17", - "@vue/compiler-ssr": "3.5.17", - "@vue/shared": "3.5.17", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.17", - "postcss": "^8.5.6", - "source-map-js": "^1.2.1" - } - }, - "@vue/compiler-ssr": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz", - "integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==", - "requires": { - "@vue/compiler-dom": "3.5.17", - "@vue/shared": "3.5.17" - } - }, - "@vue/reactivity": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.17.tgz", - "integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==", - "requires": { - "@vue/shared": "3.5.17" - } - }, - "@vue/runtime-core": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.17.tgz", - "integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==", - "requires": { - "@vue/reactivity": "3.5.17", - "@vue/shared": "3.5.17" - } - }, - "@vue/runtime-dom": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz", - "integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==", - "requires": { - "@vue/reactivity": "3.5.17", - "@vue/runtime-core": "3.5.17", - "@vue/shared": "3.5.17", - "csstype": "^3.1.3" - } - }, - "@vue/server-renderer": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.17.tgz", - "integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==", - "requires": { - "@vue/compiler-ssr": "3.5.17", - "@vue/shared": "3.5.17" - } - }, - "@vue/shared": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz", - "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==" - }, - "csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" - }, - "entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" - }, - "esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "requires": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "optional": true - }, - "magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "requires": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==" - }, - "picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "postcss": { - "version": "8.5.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.18.tgz", - "integrity": "sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==", - "requires": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - } - }, - "rollup": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", - "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", - "requires": { - "fsevents": "~2.3.2" - } - }, - "source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" - }, - "vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", - "requires": { - "esbuild": "^0.18.10", - "fsevents": "~2.3.2", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - } - }, - "vue": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz", - "integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==", - "requires": { - "@vue/compiler-dom": "3.5.17", - "@vue/compiler-sfc": "3.5.17", - "@vue/runtime-dom": "3.5.17", - "@vue/server-renderer": "3.5.17", - "@vue/shared": "3.5.17" - } - } - } -} diff --git a/authserver/web/package.json b/authserver/web/package.json deleted file mode 100644 index 3f7d557..0000000 --- a/authserver/web/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "authserver-web", - "version": "0.1.0", - "private": true, - "type": "module", - "engines": { - "node": ">=14.18.0" - }, - "scripts": { - "dev": "vite --host 0.0.0.0", - "build": "vite build", - "preview": "vite preview --host 0.0.0.0" - }, - "dependencies": { - "@vitejs/plugin-vue": "4.6.2", - "vite": "4.5.3", - "vue": "3.5.17" - }, - "devDependencies": {} -} diff --git a/authserver/web/src/App.vue b/authserver/web/src/App.vue deleted file mode 100644 index be8f366..0000000 --- a/authserver/web/src/App.vue +++ /dev/null @@ -1,219 +0,0 @@ - - - diff --git a/authserver/web/src/main.js b/authserver/web/src/main.js deleted file mode 100644 index 2425c0f..0000000 --- a/authserver/web/src/main.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createApp } from 'vue' -import './style.css' -import App from './App.vue' - -createApp(App).mount('#app') diff --git a/authserver/web/src/style.css b/authserver/web/src/style.css deleted file mode 100644 index 9283195..0000000 --- a/authserver/web/src/style.css +++ /dev/null @@ -1,206 +0,0 @@ -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-width: 320px; - min-height: 100vh; - color: #172033; - background: #f5f7fb; - font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", - sans-serif; -} - -button, -input { - font: inherit; -} - -button { - min-height: 36px; - border: 1px solid #cfd7e6; - border-radius: 6px; - background: #ffffff; - color: #172033; - cursor: pointer; -} - -button:disabled { - cursor: not-allowed; - opacity: 0.55; -} - -input { - width: 100%; - min-height: 36px; - border: 1px solid #cfd7e6; - border-radius: 6px; - padding: 0 10px; - background: #ffffff; - color: #172033; -} - -h1, -p { - margin: 0; -} - -h1 { - font-size: 22px; - line-height: 1.2; -} - -.login-page { - min-height: 100vh; - display: grid; - place-items: center; - padding: 24px; -} - -.login-panel { - width: min(420px, 100%); - display: grid; - gap: 22px; - border: 1px solid #dbe3ef; - border-radius: 8px; - padding: 28px; - background: #ffffff; - box-shadow: 0 18px 50px rgb(43 57 84 / 10%); -} - -.primary { - border-color: #1b64d8; - background: #1b64d8; - color: #ffffff; - font-weight: 650; -} - -.secondary { - padding: 0 14px; - border-color: #b8c5d9; - background: #ffffff; -} - -.eyebrow { - margin-bottom: 6px; - color: #526174; - font-size: 12px; - font-weight: 700; - letter-spacing: 0; - text-transform: uppercase; -} - -.message { - margin-top: 4px; - color: #31516f; - font-size: 13px; -} - -.app-page { - min-height: 100vh; - display: grid; - grid-template-rows: auto 1fr; - background: - linear-gradient(135deg, rgb(22 82 170 / 8%), transparent 42%), - #f5f7fb; -} - -.app-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 22px 28px; - border-bottom: 1px solid #dbe3ef; - background: rgb(255 255 255 / 86%); - backdrop-filter: blur(10px); -} - -.app-main { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 360px)); - justify-content: center; - align-items: center; - gap: 18px; - padding: 32px 20px; -} - -.app-button { - width: min(280px, 78vw); - aspect-ratio: 1; - display: grid; - place-items: center; - align-content: center; - gap: 18px; - border-color: #cfdaf0; - border-radius: 8px; - background: #ffffff; - box-shadow: 0 18px 50px rgb(43 57 84 / 12%); -} - -.app-button:hover:not(:disabled), -.app-button:focus-visible { - border-color: #1b64d8; - box-shadow: 0 22px 60px rgb(27 100 216 / 18%); - outline: none; -} - -.app-icon { - width: 112px; - height: 112px; - display: grid; - place-items: center; - border-radius: 28px; - color: #ffffff; - font-size: 64px; - font-weight: 800; - line-height: 1; -} - -.wayen-icon { - background: #1b64d8; -} - -.clouddm-icon { - background: #0f7b6c; -} - -.app-title { - color: #172033; - font-size: 22px; - font-weight: 750; -} - -.user-block { - display: flex; - align-items: center; - gap: 12px; - color: #526174; - font-size: 14px; -} - -@media (max-width: 560px) { - .app-header { - align-items: flex-start; - flex-direction: column; - padding: 18px 20px; - } - - .user-block { - width: 100%; - justify-content: space-between; - } - - .app-icon { - width: 92px; - height: 92px; - border-radius: 22px; - font-size: 52px; - } - - .app-main { - grid-template-columns: 1fr; - } -} diff --git a/authserver/web/vite.config.js b/authserver/web/vite.config.js deleted file mode 100644 index 6cb45ed..0000000 --- a/authserver/web/vite.config.js +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' - -export default defineConfig({ - base: '/auth/', - plugins: [vue()], - server: { - port: 5173, - proxy: { - '/auth/api': 'http://127.0.0.1:8083', - '/healthz': 'http://127.0.0.1:8083', - '/readyz': 'http://127.0.0.1:8083', - }, - }, -}) From 8d67ba781014592191eed6a1270fb3ef8470cbf4 Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 11:44:21 +0800 Subject: [PATCH 05/13] fix(auth): limit nginx template envsubst --- authserver/k8s/nginx.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/authserver/k8s/nginx.yaml b/authserver/k8s/nginx.yaml index fae0081..cc5c601 100644 --- a/authserver/k8s/nginx.yaml +++ b/authserver/k8s/nginx.yaml @@ -105,6 +105,8 @@ spec: ports: - containerPort: 80 env: + - name: NGINX_ENVSUBST_FILTER + value: "^(AUTHSERVER_BACKEND_UPSTREAM|CLOUDDM_UPSTREAM|CLOUDDM_PUBLIC_HOST)$" - name: AUTHSERVER_BACKEND_UPSTREAM value: http://authserver-backend:8083 - name: CLOUDDM_UPSTREAM From e212c0cd76146d8906ad44347e74e6a90225ff6b Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 11:54:25 +0800 Subject: [PATCH 06/13] fix(auth): serve unified frontend from nginx --- authserver/Dockerfile.nginx | 15 +++++++++++++++ authserver/k8s/nginx.yaml | 6 ++++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 authserver/Dockerfile.nginx diff --git a/authserver/Dockerfile.nginx b/authserver/Dockerfile.nginx new file mode 100644 index 0000000..31a43a8 --- /dev/null +++ b/authserver/Dockerfile.nginx @@ -0,0 +1,15 @@ +FROM node:20-alpine AS builder + +WORKDIR /app/frontend + +COPY frontend/package*.json ./ +RUN npm ci --no-audit --no-fund + +COPY frontend/ ./ +RUN npm run build + +FROM nginx:1.29-alpine + +COPY --from=builder /app/frontend/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/authserver/k8s/nginx.yaml b/authserver/k8s/nginx.yaml index cc5c601..9857395 100644 --- a/authserver/k8s/nginx.yaml +++ b/authserver/k8s/nginx.yaml @@ -10,6 +10,8 @@ data: server_name _; client_max_body_size 10m; + root /usr/share/nginx/html; + index index.html; location = /healthz { proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; @@ -79,7 +81,7 @@ data: } location / { - return 404; + try_files $uri $uri/ /index.html; } } --- @@ -100,7 +102,7 @@ spec: spec: containers: - name: nginx - image: nginx:1.29-alpine + image: docker.io/library/authserver-nginx:1.2.3 imagePullPolicy: IfNotPresent ports: - containerPort: 80 From c0258ae4b53008802fc076f909750ccdb8c71e54 Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 12:00:16 +0800 Subject: [PATCH 07/13] fix(frontend): make production build pass --- frontend/package-lock.json | 6 +++--- frontend/src/components/AuditLogTable.vue | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4abfa6f..f6ddc52 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2016,9 +2016,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "devOptional": true, "license": "Apache-2.0", "bin": { diff --git a/frontend/src/components/AuditLogTable.vue b/frontend/src/components/AuditLogTable.vue index e568768..22ea6ad 100644 --- a/frontend/src/components/AuditLogTable.vue +++ b/frontend/src/components/AuditLogTable.vue @@ -52,13 +52,15 @@ interface Column { class?: string } -defineProps<{ +withDefaults(defineProps<{ title: string columns: Column[] data: any[] total: number pageSize?: number -}>() +}>(), { + pageSize: 10, +}) const statusMap: Record = { success: '成功', From 093a451e0ad2292570c74ef743b99cb94b2e70d6 Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 13:27:04 +0800 Subject: [PATCH 08/13] Revert "fix(frontend): make production build pass" This reverts commit c0258ae4b53008802fc076f909750ccdb8c71e54. --- frontend/package-lock.json | 6 +++--- frontend/src/components/AuditLogTable.vue | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f6ddc52..4abfa6f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2016,9 +2016,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", "bin": { diff --git a/frontend/src/components/AuditLogTable.vue b/frontend/src/components/AuditLogTable.vue index 22ea6ad..e568768 100644 --- a/frontend/src/components/AuditLogTable.vue +++ b/frontend/src/components/AuditLogTable.vue @@ -52,15 +52,13 @@ interface Column { class?: string } -withDefaults(defineProps<{ +defineProps<{ title: string columns: Column[] data: any[] total: number pageSize?: number -}>(), { - pageSize: 10, -}) +}>() const statusMap: Record = { success: '成功', From 666e4ab762ffa88fd0e419d134faa5fe83e553a1 Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 13:27:04 +0800 Subject: [PATCH 09/13] Revert "fix(auth): serve unified frontend from nginx" This reverts commit e212c0cd76146d8906ad44347e74e6a90225ff6b. --- authserver/Dockerfile.nginx | 15 --------------- authserver/k8s/nginx.yaml | 6 ++---- 2 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 authserver/Dockerfile.nginx diff --git a/authserver/Dockerfile.nginx b/authserver/Dockerfile.nginx deleted file mode 100644 index 31a43a8..0000000 --- a/authserver/Dockerfile.nginx +++ /dev/null @@ -1,15 +0,0 @@ -FROM node:20-alpine AS builder - -WORKDIR /app/frontend - -COPY frontend/package*.json ./ -RUN npm ci --no-audit --no-fund - -COPY frontend/ ./ -RUN npm run build - -FROM nginx:1.29-alpine - -COPY --from=builder /app/frontend/dist /usr/share/nginx/html - -EXPOSE 80 diff --git a/authserver/k8s/nginx.yaml b/authserver/k8s/nginx.yaml index 9857395..cc5c601 100644 --- a/authserver/k8s/nginx.yaml +++ b/authserver/k8s/nginx.yaml @@ -10,8 +10,6 @@ data: server_name _; client_max_body_size 10m; - root /usr/share/nginx/html; - index index.html; location = /healthz { proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; @@ -81,7 +79,7 @@ data: } location / { - try_files $uri $uri/ /index.html; + return 404; } } --- @@ -102,7 +100,7 @@ spec: spec: containers: - name: nginx - image: docker.io/library/authserver-nginx:1.2.3 + image: nginx:1.29-alpine imagePullPolicy: IfNotPresent ports: - containerPort: 80 From 39a676830ce80b907139ba1ab8cee351d29975d7 Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 13:27:04 +0800 Subject: [PATCH 10/13] Revert "fix(auth): limit nginx template envsubst" This reverts commit 8d67ba781014592191eed6a1270fb3ef8470cbf4. --- authserver/k8s/nginx.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/authserver/k8s/nginx.yaml b/authserver/k8s/nginx.yaml index cc5c601..fae0081 100644 --- a/authserver/k8s/nginx.yaml +++ b/authserver/k8s/nginx.yaml @@ -105,8 +105,6 @@ spec: ports: - containerPort: 80 env: - - name: NGINX_ENVSUBST_FILTER - value: "^(AUTHSERVER_BACKEND_UPSTREAM|CLOUDDM_UPSTREAM|CLOUDDM_PUBLIC_HOST)$" - name: AUTHSERVER_BACKEND_UPSTREAM value: http://authserver-backend:8083 - name: CLOUDDM_UPSTREAM From 8203beb8cd60a5dd43a03a1b6a79afca4f640b64 Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 13:27:04 +0800 Subject: [PATCH 11/13] Revert "fix(auth): move nginx config to k8s" This reverts commit afe6c409013a1e8881f2532b0b1b1640362d206c. --- authserver/.gitignore | 2 +- authserver/cmd/server/main.go | 37 --- authserver/internal/handler/saml.go | 8 +- authserver/k8s/nginx.yaml | 148 ----------- authserver/web/.dockerignore | 4 + authserver/web/Dockerfile | 16 ++ authserver/web/index.html | 12 + authserver/web/nginx.conf | 90 +++++++ authserver/web/package-lock.json | 379 ++++++++++++++++++++++++++++ authserver/web/package.json | 20 ++ authserver/web/src/App.vue | 219 ++++++++++++++++ authserver/web/src/main.js | 5 + authserver/web/src/style.css | 206 +++++++++++++++ authserver/web/vite.config.js | 15 ++ 14 files changed, 971 insertions(+), 190 deletions(-) delete mode 100644 authserver/cmd/server/main.go delete mode 100644 authserver/k8s/nginx.yaml create mode 100644 authserver/web/.dockerignore create mode 100644 authserver/web/Dockerfile create mode 100644 authserver/web/index.html create mode 100644 authserver/web/nginx.conf create mode 100644 authserver/web/package-lock.json create mode 100644 authserver/web/package.json create mode 100644 authserver/web/src/App.vue create mode 100644 authserver/web/src/main.js create mode 100644 authserver/web/src/style.css create mode 100644 authserver/web/vite.config.js diff --git a/authserver/.gitignore b/authserver/.gitignore index 6097006..eca0741 100644 --- a/authserver/.gitignore +++ b/authserver/.gitignore @@ -5,7 +5,7 @@ .DS_Store *.tar bin/ -/server +server web/node_modules/ web/dist/ certs/ diff --git a/authserver/cmd/server/main.go b/authserver/cmd/server/main.go deleted file mode 100644 index 7ad3580..0000000 --- a/authserver/cmd/server/main.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "log" - - "authserver/internal/config" - "authserver/internal/database" - "authserver/internal/router" -) - -func main() { - cfg := config.Load() - - db, err := database.Open(cfg.MySQLDSN) - if err != nil { - log.Fatalf("open database: %v", err) - } - - if cfg.AutoMigrate { - if err := database.AutoMigrate(db); err != nil { - log.Fatalf("auto migrate: %v", err) - } - } - if err := database.SeedDefaults(db, cfg); err != nil { - log.Fatalf("seed defaults: %v", err) - } - - r := router.New(router.Dependencies{ - Config: cfg, - DB: db, - }) - - log.Printf("authserver listening on %s", cfg.HTTPAddr) - if err := r.Run(cfg.HTTPAddr); err != nil { - log.Fatalf("run server: %v", err) - } -} diff --git a/authserver/internal/handler/saml.go b/authserver/internal/handler/saml.go index 0ca7f23..a90818d 100644 --- a/authserver/internal/handler/saml.go +++ b/authserver/internal/handler/saml.go @@ -97,15 +97,15 @@ func (h *SAMLHandler) ACS(c *gin.Context) { func ssoRedirectURL(relayState, token string) string { target := strings.TrimSpace(relayState) - if target == "" || !strings.HasPrefix(target, "/") { - target = "/" + if target == "" || !strings.HasPrefix(target, "/auth/") { + target = "/auth/" } if strings.HasPrefix(target, "//") { - target = "/" + target = "/auth/" } parsed, err := url.Parse(target) if err != nil || parsed.IsAbs() { - parsed = &url.URL{Path: "/"} + parsed = &url.URL{Path: "/auth/"} } values := parsed.Query() values.Set("sso_token", token) diff --git a/authserver/k8s/nginx.yaml b/authserver/k8s/nginx.yaml deleted file mode 100644 index fae0081..0000000 --- a/authserver/k8s/nginx.yaml +++ /dev/null @@ -1,148 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: authserver-nginx-conf - namespace: authserver -data: - default.conf.template: | - server { - listen 80; - server_name _; - - client_max_body_size 10m; - - location = /healthz { - proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location = /readyz { - proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /auth/api/ { - proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 5s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - location /auth/oauth/ { - proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 5s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - location /auth/.well-known/ { - proxy_pass ${AUTHSERVER_BACKEND_UPSTREAM}; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_connect_timeout 5s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - location /internal/clouddm/ { - proxy_pass ${CLOUDDM_UPSTREAM}/; - proxy_http_version 1.1; - proxy_set_header Host ${CLOUDDM_PUBLIC_HOST}; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Host ${CLOUDDM_PUBLIC_HOST}; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto http; - proxy_connect_timeout 5s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - location / { - return 404; - } - } ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: authserver-nginx - namespace: authserver -spec: - replicas: 1 - selector: - matchLabels: - app: authserver-nginx - template: - metadata: - labels: - app: authserver-nginx - spec: - containers: - - name: nginx - image: nginx:1.29-alpine - imagePullPolicy: IfNotPresent - ports: - - containerPort: 80 - env: - - name: AUTHSERVER_BACKEND_UPSTREAM - value: http://authserver-backend:8083 - - name: CLOUDDM_UPSTREAM - value: http://open-cdm.db.svc.cluster.local:8222 - - name: CLOUDDM_PUBLIC_HOST - value: 218.11.5.223:30009 - volumeMounts: - - name: nginx-conf - mountPath: /etc/nginx/templates/default.conf.template - subPath: default.conf.template - readinessProbe: - httpGet: - path: /readyz - port: 80 - initialDelaySeconds: 5 - periodSeconds: 10 - livenessProbe: - httpGet: - path: /healthz - port: 80 - initialDelaySeconds: 10 - periodSeconds: 20 - volumes: - - name: nginx-conf - configMap: - name: authserver-nginx-conf ---- -apiVersion: v1 -kind: Service -metadata: - name: authserver-nginx - namespace: authserver -spec: - type: NodePort - selector: - app: authserver-nginx - ports: - - name: http - port: 80 - targetPort: 80 - nodePort: 30080 diff --git a/authserver/web/.dockerignore b/authserver/web/.dockerignore new file mode 100644 index 0000000..a7e5733 --- /dev/null +++ b/authserver/web/.dockerignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.DS_Store + diff --git a/authserver/web/Dockerfile b/authserver/web/Dockerfile new file mode 100644 index 0000000..debcaaf --- /dev/null +++ b/authserver/web/Dockerfile @@ -0,0 +1,16 @@ +FROM node:14-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm install --no-audit --no-fund + +COPY . . +RUN npm run build + +FROM nginx:1.29-alpine + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html/auth + +EXPOSE 80 diff --git a/authserver/web/index.html b/authserver/web/index.html new file mode 100644 index 0000000..f469a73 --- /dev/null +++ b/authserver/web/index.html @@ -0,0 +1,12 @@ + + + + + + AuthServer + + +
+ + + diff --git a/authserver/web/nginx.conf b/authserver/web/nginx.conf new file mode 100644 index 0000000..caaae48 --- /dev/null +++ b/authserver/web/nginx.conf @@ -0,0 +1,90 @@ +server { + listen 80; + server_name _; + + client_max_body_size 10m; + + root /usr/share/nginx/html; + index index.html; + + location = / { + return 302 /auth/; + } + + location = /healthz { + proxy_pass http://authserver-backend:8083; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /readyz { + proxy_pass http://authserver-backend:8083; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /auth/api/ { + proxy_pass http://authserver-backend:8083; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /auth/oauth/ { + proxy_pass http://authserver-backend:8083; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /auth/.well-known/ { + proxy_pass http://authserver-backend:8083; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /internal/clouddm/ { + proxy_pass http://open-cdm.db.svc.cluster.local:8222/; + proxy_http_version 1.1; + proxy_set_header Host 218.11.5.223:30009; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Host 218.11.5.223:30009; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto http; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location /auth/ { + try_files $uri $uri/ /auth/index.html; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { + expires 7d; + add_header Cache-Control "public, max-age=604800"; + try_files $uri =404; + } +} diff --git a/authserver/web/package-lock.json b/authserver/web/package-lock.json new file mode 100644 index 0000000..6c2df8d --- /dev/null +++ b/authserver/web/package-lock.json @@ -0,0 +1,379 @@ +{ + "name": "authserver-web", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==" + }, + "@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" + }, + "@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "requires": { + "@babel/types": "^7.29.7" + } + }, + "@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "requires": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + } + }, + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "optional": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==" + }, + "@vue/compiler-core": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.17.tgz", + "integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==", + "requires": { + "@babel/parser": "^7.27.5", + "@vue/shared": "3.5.17", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "@vue/compiler-dom": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz", + "integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==", + "requires": { + "@vue/compiler-core": "3.5.17", + "@vue/shared": "3.5.17" + } + }, + "@vue/compiler-sfc": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz", + "integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==", + "requires": { + "@babel/parser": "^7.27.5", + "@vue/compiler-core": "3.5.17", + "@vue/compiler-dom": "3.5.17", + "@vue/compiler-ssr": "3.5.17", + "@vue/shared": "3.5.17", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "@vue/compiler-ssr": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz", + "integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==", + "requires": { + "@vue/compiler-dom": "3.5.17", + "@vue/shared": "3.5.17" + } + }, + "@vue/reactivity": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.17.tgz", + "integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==", + "requires": { + "@vue/shared": "3.5.17" + } + }, + "@vue/runtime-core": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.17.tgz", + "integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==", + "requires": { + "@vue/reactivity": "3.5.17", + "@vue/shared": "3.5.17" + } + }, + "@vue/runtime-dom": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz", + "integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==", + "requires": { + "@vue/reactivity": "3.5.17", + "@vue/runtime-core": "3.5.17", + "@vue/shared": "3.5.17", + "csstype": "^3.1.3" + } + }, + "@vue/server-renderer": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.17.tgz", + "integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==", + "requires": { + "@vue/compiler-ssr": "3.5.17", + "@vue/shared": "3.5.17" + } + }, + "@vue/shared": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz", + "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==" + }, + "csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "requires": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, + "magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==" + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "postcss": { + "version": "8.5.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.18.tgz", + "integrity": "sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==", + "requires": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "rollup": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", + "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", + "requires": { + "fsevents": "~2.3.2" + } + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "requires": { + "esbuild": "^0.18.10", + "fsevents": "~2.3.2", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + } + }, + "vue": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz", + "integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==", + "requires": { + "@vue/compiler-dom": "3.5.17", + "@vue/compiler-sfc": "3.5.17", + "@vue/runtime-dom": "3.5.17", + "@vue/server-renderer": "3.5.17", + "@vue/shared": "3.5.17" + } + } + } +} diff --git a/authserver/web/package.json b/authserver/web/package.json new file mode 100644 index 0000000..3f7d557 --- /dev/null +++ b/authserver/web/package.json @@ -0,0 +1,20 @@ +{ + "name": "authserver-web", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": ">=14.18.0" + }, + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vite build", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@vitejs/plugin-vue": "4.6.2", + "vite": "4.5.3", + "vue": "3.5.17" + }, + "devDependencies": {} +} diff --git a/authserver/web/src/App.vue b/authserver/web/src/App.vue new file mode 100644 index 0000000..be8f366 --- /dev/null +++ b/authserver/web/src/App.vue @@ -0,0 +1,219 @@ + + + diff --git a/authserver/web/src/main.js b/authserver/web/src/main.js new file mode 100644 index 0000000..2425c0f --- /dev/null +++ b/authserver/web/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/authserver/web/src/style.css b/authserver/web/src/style.css new file mode 100644 index 0000000..9283195 --- /dev/null +++ b/authserver/web/src/style.css @@ -0,0 +1,206 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + color: #172033; + background: #f5f7fb; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; +} + +button, +input { + font: inherit; +} + +button { + min-height: 36px; + border: 1px solid #cfd7e6; + border-radius: 6px; + background: #ffffff; + color: #172033; + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +input { + width: 100%; + min-height: 36px; + border: 1px solid #cfd7e6; + border-radius: 6px; + padding: 0 10px; + background: #ffffff; + color: #172033; +} + +h1, +p { + margin: 0; +} + +h1 { + font-size: 22px; + line-height: 1.2; +} + +.login-page { + min-height: 100vh; + display: grid; + place-items: center; + padding: 24px; +} + +.login-panel { + width: min(420px, 100%); + display: grid; + gap: 22px; + border: 1px solid #dbe3ef; + border-radius: 8px; + padding: 28px; + background: #ffffff; + box-shadow: 0 18px 50px rgb(43 57 84 / 10%); +} + +.primary { + border-color: #1b64d8; + background: #1b64d8; + color: #ffffff; + font-weight: 650; +} + +.secondary { + padding: 0 14px; + border-color: #b8c5d9; + background: #ffffff; +} + +.eyebrow { + margin-bottom: 6px; + color: #526174; + font-size: 12px; + font-weight: 700; + letter-spacing: 0; + text-transform: uppercase; +} + +.message { + margin-top: 4px; + color: #31516f; + font-size: 13px; +} + +.app-page { + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr; + background: + linear-gradient(135deg, rgb(22 82 170 / 8%), transparent 42%), + #f5f7fb; +} + +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 22px 28px; + border-bottom: 1px solid #dbe3ef; + background: rgb(255 255 255 / 86%); + backdrop-filter: blur(10px); +} + +.app-main { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 360px)); + justify-content: center; + align-items: center; + gap: 18px; + padding: 32px 20px; +} + +.app-button { + width: min(280px, 78vw); + aspect-ratio: 1; + display: grid; + place-items: center; + align-content: center; + gap: 18px; + border-color: #cfdaf0; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 18px 50px rgb(43 57 84 / 12%); +} + +.app-button:hover:not(:disabled), +.app-button:focus-visible { + border-color: #1b64d8; + box-shadow: 0 22px 60px rgb(27 100 216 / 18%); + outline: none; +} + +.app-icon { + width: 112px; + height: 112px; + display: grid; + place-items: center; + border-radius: 28px; + color: #ffffff; + font-size: 64px; + font-weight: 800; + line-height: 1; +} + +.wayen-icon { + background: #1b64d8; +} + +.clouddm-icon { + background: #0f7b6c; +} + +.app-title { + color: #172033; + font-size: 22px; + font-weight: 750; +} + +.user-block { + display: flex; + align-items: center; + gap: 12px; + color: #526174; + font-size: 14px; +} + +@media (max-width: 560px) { + .app-header { + align-items: flex-start; + flex-direction: column; + padding: 18px 20px; + } + + .user-block { + width: 100%; + justify-content: space-between; + } + + .app-icon { + width: 92px; + height: 92px; + border-radius: 22px; + font-size: 52px; + } + + .app-main { + grid-template-columns: 1fr; + } +} diff --git a/authserver/web/vite.config.js b/authserver/web/vite.config.js new file mode 100644 index 0000000..6cb45ed --- /dev/null +++ b/authserver/web/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + base: '/auth/', + plugins: [vue()], + server: { + port: 5173, + proxy: { + '/auth/api': 'http://127.0.0.1:8083', + '/healthz': 'http://127.0.0.1:8083', + '/readyz': 'http://127.0.0.1:8083', + }, + }, +}) From 6af511ed0bf0f9c931c7b3c4cb66eb723bf7a2f0 Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 14:05:22 +0800 Subject: [PATCH 12/13] refactor(server): merge authserver implementation --- authserver/go.mod | 48 ------- authserver/go.sum | 102 --------------- frontend/src/types/api.ts | 2 +- {authserver => server}/.dockerignore | 0 {authserver => server}/.env.example | 0 {authserver => server}/.gitignore | 2 +- {authserver => server}/Dockerfile.backend | 0 {authserver => server}/README.md | 0 server/cmd/server/.gitkeep | 4 - server/cmd/server/main.go | 101 ++++----------- .../docker-compose.mysql.yml | 0 server/go.mod | 58 ++++++--- server/go.sum | 122 ++++++++++-------- {authserver => server}/internal/auth/jwt.go | 0 .../internal/auth/oidc_key.go | 0 .../internal/auth/random.go | 0 server/internal/config/.gitkeep | 4 - .../internal/config/config.go | 0 .../internal/database/database.go | 2 +- .../internal/database/seed.go | 2 +- server/internal/handler/.gitkeep | 8 -- .../internal/handler/clouddm.go | 4 +- .../internal/handler/context.go | 4 +- .../internal/handler/health.go | 0 .../internal/handler/oauth.go | 8 +- .../internal/handler/saml.go | 6 +- .../internal/handler/user.go | 0 .../internal/handler/wayen.go | 6 +- server/internal/middleware/.gitkeep | 6 - server/internal/middleware/logger.go | 38 ------ server/internal/middleware/recovery.go | 28 ---- server/internal/model/.gitkeep | 6 - .../internal/model/models.go | 0 server/internal/repository/.gitkeep | 5 - .../{handler => response}/response.go | 54 +++----- server/internal/router/.gitkeep | 4 - .../internal/router/router.go | 6 +- server/internal/service/.gitkeep | 8 -- .../internal/service/audit.go | 2 +- .../internal/service/auth.go | 8 +- .../internal/service/wayen.go | 4 +- {authserver => server}/internal/sso/login.go | 0 .../internal/sso/login_test.go | 0 .../internal/sso/metadata.go | 0 server/internal/websocket/.gitkeep | 4 - server/migrations/.gitkeep | 4 - server/pkg/database/.gitkeep | 4 - server/pkg/ldap/.gitkeep | 4 - server/pkg/sso/.gitkeep | 5 - {authserver => server}/web/.dockerignore | 0 {authserver => server}/web/Dockerfile | 0 {authserver => server}/web/index.html | 0 {authserver => server}/web/nginx.conf | 0 {authserver => server}/web/package-lock.json | 0 {authserver => server}/web/package.json | 0 {authserver => server}/web/src/App.vue | 0 {authserver => server}/web/src/main.js | 0 {authserver => server}/web/src/style.css | 0 {authserver => server}/web/vite.config.js | 0 59 files changed, 176 insertions(+), 497 deletions(-) delete mode 100644 authserver/go.mod delete mode 100644 authserver/go.sum rename {authserver => server}/.dockerignore (100%) rename {authserver => server}/.env.example (100%) rename {authserver => server}/.gitignore (93%) rename {authserver => server}/Dockerfile.backend (100%) rename {authserver => server}/README.md (100%) delete mode 100644 server/cmd/server/.gitkeep rename {authserver => server}/docker-compose.mysql.yml (100%) rename {authserver => server}/internal/auth/jwt.go (100%) rename {authserver => server}/internal/auth/oidc_key.go (100%) rename {authserver => server}/internal/auth/random.go (100%) delete mode 100644 server/internal/config/.gitkeep rename {authserver => server}/internal/config/config.go (100%) rename {authserver => server}/internal/database/database.go (85%) rename {authserver => server}/internal/database/seed.go (66%) delete mode 100644 server/internal/handler/.gitkeep rename {authserver => server}/internal/handler/clouddm.go (96%) rename {authserver => server}/internal/handler/context.go (87%) rename {authserver => server}/internal/handler/health.go (100%) rename {authserver => server}/internal/handler/oauth.go (97%) rename {authserver => server}/internal/handler/saml.go (94%) rename {authserver => server}/internal/handler/user.go (100%) rename {authserver => server}/internal/handler/wayen.go (96%) delete mode 100644 server/internal/middleware/.gitkeep delete mode 100644 server/internal/middleware/logger.go delete mode 100644 server/internal/middleware/recovery.go delete mode 100644 server/internal/model/.gitkeep rename {authserver => server}/internal/model/models.go (100%) delete mode 100644 server/internal/repository/.gitkeep rename server/internal/{handler => response}/response.go (56%) delete mode 100644 server/internal/router/.gitkeep rename {authserver => server}/internal/router/router.go (91%) delete mode 100644 server/internal/service/.gitkeep rename {authserver => server}/internal/service/audit.go (95%) rename {authserver => server}/internal/service/auth.go (96%) rename {authserver => server}/internal/service/wayen.go (97%) rename {authserver => server}/internal/sso/login.go (100%) rename {authserver => server}/internal/sso/login_test.go (100%) rename {authserver => server}/internal/sso/metadata.go (100%) delete mode 100644 server/internal/websocket/.gitkeep delete mode 100644 server/migrations/.gitkeep delete mode 100644 server/pkg/database/.gitkeep delete mode 100644 server/pkg/ldap/.gitkeep delete mode 100644 server/pkg/sso/.gitkeep rename {authserver => server}/web/.dockerignore (100%) rename {authserver => server}/web/Dockerfile (100%) rename {authserver => server}/web/index.html (100%) rename {authserver => server}/web/nginx.conf (100%) rename {authserver => server}/web/package-lock.json (100%) rename {authserver => server}/web/package.json (100%) rename {authserver => server}/web/src/App.vue (100%) rename {authserver => server}/web/src/main.js (100%) rename {authserver => server}/web/src/style.css (100%) rename {authserver => server}/web/vite.config.js (100%) diff --git a/authserver/go.mod b/authserver/go.mod deleted file mode 100644 index 16af137..0000000 --- a/authserver/go.mod +++ /dev/null @@ -1,48 +0,0 @@ -module authserver - -go 1.26 - -require ( - github.com/gin-gonic/gin v1.11.0 - github.com/golang-jwt/jwt/v5 v5.3.0 - golang.org/x/crypto v0.40.0 - gorm.io/driver/mysql v1.6.0 - gorm.io/gorm v1.30.1 -) - -require ( - filippo.io/edwards25519 v1.1.0 // indirect - github.com/bytedance/sonic v1.14.0 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect - github.com/cloudwego/base64x v0.1.6 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/goccy/go-yaml v1.18.0 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.0 // indirect - go.uber.org/mock v0.5.0 // indirect - golang.org/x/arch v0.20.0 // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.34.0 // indirect - google.golang.org/protobuf v1.36.9 // indirect -) diff --git a/authserver/go.sum b/authserver/go.sum deleted file mode 100644 index d4f772e..0000000 --- a/authserver/go.sum +++ /dev/null @@ -1,102 +0,0 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= -github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= -github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= -github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= -github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= -golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -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= -gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= -gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= -gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= -gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 3ff1886..880d985 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -1,6 +1,6 @@ /** * 后端统一响应格式类型定义 - * 对应 server/internal/handler/response.go + * 对应 server/internal/response/response.go */ /** 统一 JSON 响应信封 */ diff --git a/authserver/.dockerignore b/server/.dockerignore similarity index 100% rename from authserver/.dockerignore rename to server/.dockerignore diff --git a/authserver/.env.example b/server/.env.example similarity index 100% rename from authserver/.env.example rename to server/.env.example diff --git a/authserver/.gitignore b/server/.gitignore similarity index 93% rename from authserver/.gitignore rename to server/.gitignore index eca0741..6097006 100644 --- a/authserver/.gitignore +++ b/server/.gitignore @@ -5,7 +5,7 @@ .DS_Store *.tar bin/ -server +/server web/node_modules/ web/dist/ certs/ diff --git a/authserver/Dockerfile.backend b/server/Dockerfile.backend similarity index 100% rename from authserver/Dockerfile.backend rename to server/Dockerfile.backend diff --git a/authserver/README.md b/server/README.md similarity index 100% rename from authserver/README.md rename to server/README.md diff --git a/server/cmd/server/.gitkeep b/server/cmd/server/.gitkeep deleted file mode 100644 index efbb44f..0000000 --- a/server/cmd/server/.gitkeep +++ /dev/null @@ -1,4 +0,0 @@ -# 服务启动入口 - -存放 Go 程序入口文件: -- main.go: 服务启动入口,初始化配置、数据库、路由,启动 HTTP 服务 diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index b13787d..d7db5b6 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -1,90 +1,37 @@ package main import ( - "fmt" - "net/http" - "strconv" - "time" + "log" - "github.com/1024XEngineer/xinfra/server/internal/handler" - "github.com/1024XEngineer/xinfra/server/internal/middleware" - "github.com/gin-gonic/gin" + "github.com/1024XEngineer/xinfra/server/internal/config" + "github.com/1024XEngineer/xinfra/server/internal/database" + "github.com/1024XEngineer/xinfra/server/internal/router" ) func main() { - r := gin.Default() + cfg := config.Load() - // 加载中间件 - r.Use(middleware.Logger()) - r.Use(middleware.Recovery()) + db, err := database.Open(cfg.MySQLDSN) + if err != nil { + log.Fatalf("open database: %v", err) + } - // 健康检查 - r.GET("/api/v1/ping", func(c *gin.Context) { - handler.Success(c, gin.H{"time": time.Now().Format(time.RFC3339)}) + if cfg.AutoMigrate { + if err := database.AutoMigrate(db); err != nil { + log.Fatalf("auto migrate: %v", err) + } + } + if err := database.SeedDefaults(db, cfg); err != nil { + log.Fatalf("seed defaults: %v", err) + } + + r := router.New(router.Dependencies{ + Config: cfg, + DB: db, }) - // 测试路由组 — 用于验证统一响应格式 - testGroup := r.Group("/api/v1/test") - { - // 成功响应 - testGroup.GET("/success", func(c *gin.Context) { - handler.Success(c, gin.H{ - "username": "testuser", - "role": "admin", - }) - }) - - // 业务错误 — 传入错误码 - // GET /api/v1/test/error/10001 → LDAP 认证失败 - // GET /api/v1/test/error/10002 → 用户名或密码错误 - // GET /api/v1/test/error/10003 → Token 已过期 - // GET /api/v1/test/error/20001 → 任务创建失败 - // GET /api/v1/test/error/20002 → 任务执行超时 - testGroup.GET("/error/:code", func(c *gin.Context) { - codeStr := c.Param("code") - code, err := strconv.Atoi(codeStr) - if err != nil { - handler.BadRequest(c, "错误码必须是数字") - return - } - details := fmt.Sprintf("这是错误码 %d 的测试响应", code) - handler.Error(c, http.StatusOK, code, details) - }) - - // HTTP 500 错误 - testGroup.GET("/500", func(c *gin.Context) { - handler.InternalError(c, "模拟服务器内部错误") - }) - - // HTTP 401 未授权 - testGroup.GET("/401", func(c *gin.Context) { - handler.Unauthorized(c, "模拟 Token 过期") - }) - - // HTTP 403 禁止访问 - testGroup.GET("/403", func(c *gin.Context) { - handler.Forbidden(c, "模拟无权限") - }) - - // 模拟超时(sleep 超过前端 10s timeout) - testGroup.GET("/timeout", func(c *gin.Context) { - time.Sleep(15 * time.Second) - handler.Success(c, "这条消息不应该出现") - }) - - // 返回分页数据 - testGroup.GET("/paginated", func(c *gin.Context) { - items := []gin.H{ - {"id": 1, "name": "item-1"}, - {"id": 2, "name": "item-2"}, - {"id": 3, "name": "item-3"}, - } - handler.SuccessWithPaginated(c, 100, items) - }) - } - - fmt.Println("Server starting on :8080...") - if err := r.Run(":8080"); err != nil { - panic(err) + log.Printf("authserver listening on %s", cfg.HTTPAddr) + if err := r.Run(cfg.HTTPAddr); err != nil { + log.Fatalf("run server: %v", err) } } diff --git a/authserver/docker-compose.mysql.yml b/server/docker-compose.mysql.yml similarity index 100% rename from authserver/docker-compose.mysql.yml rename to server/docker-compose.mysql.yml diff --git a/server/go.mod b/server/go.mod index 455aa12..8fe33ab 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,32 +1,48 @@ module github.com/1024XEngineer/xinfra/server -go 1.21 - -require github.com/gin-gonic/gin v1.9.1 +go 1.26 require ( - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.0 + gorm.io/driver/mysql v1.6.0 + gorm.io/gorm v1.30.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/server/go.sum b/server/go.sum index 1a77fa1..d4f772e 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,86 +1,102 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= +gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/authserver/internal/auth/jwt.go b/server/internal/auth/jwt.go similarity index 100% rename from authserver/internal/auth/jwt.go rename to server/internal/auth/jwt.go diff --git a/authserver/internal/auth/oidc_key.go b/server/internal/auth/oidc_key.go similarity index 100% rename from authserver/internal/auth/oidc_key.go rename to server/internal/auth/oidc_key.go diff --git a/authserver/internal/auth/random.go b/server/internal/auth/random.go similarity index 100% rename from authserver/internal/auth/random.go rename to server/internal/auth/random.go diff --git a/server/internal/config/.gitkeep b/server/internal/config/.gitkeep deleted file mode 100644 index 3f1878d..0000000 --- a/server/internal/config/.gitkeep +++ /dev/null @@ -1,4 +0,0 @@ -# 配置管理模块 - -存放配置加载相关代码: -- config.go: 配置加载,读取 YAML 配置文件,支持环境变量覆盖 diff --git a/authserver/internal/config/config.go b/server/internal/config/config.go similarity index 100% rename from authserver/internal/config/config.go rename to server/internal/config/config.go diff --git a/authserver/internal/database/database.go b/server/internal/database/database.go similarity index 85% rename from authserver/internal/database/database.go rename to server/internal/database/database.go index ca19b6e..c57fa40 100644 --- a/authserver/internal/database/database.go +++ b/server/internal/database/database.go @@ -1,7 +1,7 @@ package database import ( - "authserver/internal/model" + "github.com/1024XEngineer/xinfra/server/internal/model" "gorm.io/driver/mysql" "gorm.io/gorm" diff --git a/authserver/internal/database/seed.go b/server/internal/database/seed.go similarity index 66% rename from authserver/internal/database/seed.go rename to server/internal/database/seed.go index 2d06763..32a2d6c 100644 --- a/authserver/internal/database/seed.go +++ b/server/internal/database/seed.go @@ -1,7 +1,7 @@ package database import ( - "authserver/internal/config" + "github.com/1024XEngineer/xinfra/server/internal/config" "gorm.io/gorm" ) diff --git a/server/internal/handler/.gitkeep b/server/internal/handler/.gitkeep deleted file mode 100644 index 67a2a01..0000000 --- a/server/internal/handler/.gitkeep +++ /dev/null @@ -1,8 +0,0 @@ -# HTTP 处理器层(Controller) - -存放请求处理器,负责接收请求、校验参数、调用 Service: -- auth.go: 认证处理器,处理登录/登出/用户信息请求 -- audit.go: 审计处理器,处理登录审计/运维操作审计查询请求 -- subsystem.go: 子系统处理器,处理子系统列表/SSO URL 生成请求 -- task.go: 任务处理器,处理 Ansible 任务创建/状态查询请求 -- response.go: 统一响应,定义标准 JSON 响应格式和错误码 diff --git a/authserver/internal/handler/clouddm.go b/server/internal/handler/clouddm.go similarity index 96% rename from authserver/internal/handler/clouddm.go rename to server/internal/handler/clouddm.go index 904e6b8..d0ca6cd 100644 --- a/authserver/internal/handler/clouddm.go +++ b/server/internal/handler/clouddm.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "authserver/internal/config" - "authserver/internal/service" + "github.com/1024XEngineer/xinfra/server/internal/config" + "github.com/1024XEngineer/xinfra/server/internal/service" "github.com/gin-gonic/gin" ) diff --git a/authserver/internal/handler/context.go b/server/internal/handler/context.go similarity index 87% rename from authserver/internal/handler/context.go rename to server/internal/handler/context.go index dbb1acd..7af0641 100644 --- a/authserver/internal/handler/context.go +++ b/server/internal/handler/context.go @@ -4,8 +4,8 @@ import ( "net/http" "strings" - "authserver/internal/auth" - "authserver/internal/config" + "github.com/1024XEngineer/xinfra/server/internal/auth" + "github.com/1024XEngineer/xinfra/server/internal/config" "github.com/gin-gonic/gin" ) diff --git a/authserver/internal/handler/health.go b/server/internal/handler/health.go similarity index 100% rename from authserver/internal/handler/health.go rename to server/internal/handler/health.go diff --git a/authserver/internal/handler/oauth.go b/server/internal/handler/oauth.go similarity index 97% rename from authserver/internal/handler/oauth.go rename to server/internal/handler/oauth.go index cffa577..403b283 100644 --- a/authserver/internal/handler/oauth.go +++ b/server/internal/handler/oauth.go @@ -8,10 +8,10 @@ import ( "strings" "time" - "authserver/internal/auth" - "authserver/internal/config" - "authserver/internal/model" - "authserver/internal/service" + "github.com/1024XEngineer/xinfra/server/internal/auth" + "github.com/1024XEngineer/xinfra/server/internal/config" + "github.com/1024XEngineer/xinfra/server/internal/model" + "github.com/1024XEngineer/xinfra/server/internal/service" "github.com/gin-gonic/gin" "gorm.io/gorm" diff --git a/authserver/internal/handler/saml.go b/server/internal/handler/saml.go similarity index 94% rename from authserver/internal/handler/saml.go rename to server/internal/handler/saml.go index a90818d..4c21ed9 100644 --- a/authserver/internal/handler/saml.go +++ b/server/internal/handler/saml.go @@ -7,9 +7,9 @@ import ( "strings" "time" - "authserver/internal/config" - "authserver/internal/service" - "authserver/internal/sso" + "github.com/1024XEngineer/xinfra/server/internal/config" + "github.com/1024XEngineer/xinfra/server/internal/service" + "github.com/1024XEngineer/xinfra/server/internal/sso" "github.com/gin-gonic/gin" ) diff --git a/authserver/internal/handler/user.go b/server/internal/handler/user.go similarity index 100% rename from authserver/internal/handler/user.go rename to server/internal/handler/user.go diff --git a/authserver/internal/handler/wayen.go b/server/internal/handler/wayen.go similarity index 96% rename from authserver/internal/handler/wayen.go rename to server/internal/handler/wayen.go index 5f2718f..18c1776 100644 --- a/authserver/internal/handler/wayen.go +++ b/server/internal/handler/wayen.go @@ -5,9 +5,9 @@ import ( "net/http" "strings" - "authserver/internal/auth" - "authserver/internal/model" - "authserver/internal/service" + "github.com/1024XEngineer/xinfra/server/internal/auth" + "github.com/1024XEngineer/xinfra/server/internal/model" + "github.com/1024XEngineer/xinfra/server/internal/service" "github.com/gin-gonic/gin" "gorm.io/gorm" diff --git a/server/internal/middleware/.gitkeep b/server/internal/middleware/.gitkeep deleted file mode 100644 index 75697ab..0000000 --- a/server/internal/middleware/.gitkeep +++ /dev/null @@ -1,6 +0,0 @@ -# HTTP 中间件 - -存放请求处理前后执行的通用逻辑: -- auth.go: 认证中间件,校验 JWT Token,注入用户信息到 Context -- logger.go: 日志中间件,记录请求方法、路径、耗时、状态码 -- audit.go: 审计中间件,自动记录非 GET 请求的操作审计日志 diff --git a/server/internal/middleware/logger.go b/server/internal/middleware/logger.go deleted file mode 100644 index 5a29b07..0000000 --- a/server/internal/middleware/logger.go +++ /dev/null @@ -1,38 +0,0 @@ -package middleware - -import ( - "log" - "time" - - "github.com/gin-gonic/gin" -) - -// Logger 返回请求日志中间件 -// 记录请求方法、路径、状态码、耗时、客户端 IP -func Logger() gin.HandlerFunc { - return func(c *gin.Context) { - // 请求开始时间 - startTime := time.Now() - - // 处理请求 - c.Next() - - // 计算耗时 - duration := time.Since(startTime) - - // 获取请求信息 - method := c.Request.Method - path := c.Request.URL.Path - statusCode := c.Writer.Status() - clientIP := c.ClientIP() - - // 记录日志 - log.Printf("[REQUEST] %s %s | %d | %v | %s", - method, - path, - statusCode, - duration, - clientIP, - ) - } -} diff --git a/server/internal/middleware/recovery.go b/server/internal/middleware/recovery.go deleted file mode 100644 index c933495..0000000 --- a/server/internal/middleware/recovery.go +++ /dev/null @@ -1,28 +0,0 @@ -package middleware - -import ( - "log" - "runtime/debug" - - "github.com/1024XEngineer/xinfra/server/internal/handler" - "github.com/gin-gonic/gin" -) - -// Recovery 返回 panic 恢复中间件 -// 捕获请求处理过程中的 panic,返回 500 错误而不是崩溃 -func Recovery() gin.HandlerFunc { - return func(c *gin.Context) { - defer func() { - if err := recover(); err != nil { - // 记录 panic 堆栈信息 - log.Printf("[PANIC] %v\n%s", err, debug.Stack()) - - // 返回 500 错误 - handler.InternalError(c, "服务器内部错误") - c.Abort() - } - }() - - c.Next() - } -} diff --git a/server/internal/model/.gitkeep b/server/internal/model/.gitkeep deleted file mode 100644 index 33256f7..0000000 --- a/server/internal/model/.gitkeep +++ /dev/null @@ -1,6 +0,0 @@ -# 数据模型层 - -存放数据库表结构和业务实体定义: -- user.go: 用户模型,对应 users 表,定义用户字段和状态枚举 -- audit.go: 审计模型,对应 login_audit / ops_audit 表 -- subsystem.go: 子系统模型,对应 subsystems 表,定义子系统信息 diff --git a/authserver/internal/model/models.go b/server/internal/model/models.go similarity index 100% rename from authserver/internal/model/models.go rename to server/internal/model/models.go diff --git a/server/internal/repository/.gitkeep b/server/internal/repository/.gitkeep deleted file mode 100644 index e6a190d..0000000 --- a/server/internal/repository/.gitkeep +++ /dev/null @@ -1,5 +0,0 @@ -# 数据访问层(DAO) - -存放数据库 CRUD 操作封装: -- user.go: 用户仓储,用户查询、创建、更新、LDAP DN 映射 -- audit.go: 审计仓储,审计记录插入、分页查询、条件筛选 diff --git a/server/internal/handler/response.go b/server/internal/response/response.go similarity index 56% rename from server/internal/handler/response.go rename to server/internal/response/response.go index bfea90f..b67281c 100644 --- a/server/internal/handler/response.go +++ b/server/internal/response/response.go @@ -1,4 +1,4 @@ -package handler +package response import ( "net/http" @@ -6,38 +6,31 @@ import ( "github.com/gin-gonic/gin" ) -// Response 统一响应结构 type Response struct { - Code int `json:"code"` // 业务错误码,0 表示成功 - Message string `json:"message"` // 响应消息 - Data interface{} `json:"data,omitempty"` // 响应数据,成功时返回 - Details string `json:"details,omitempty"` // 错误详情,失败时返回 + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` + Details string `json:"details,omitempty"` } -// 分页响应结构 type PaginatedData struct { Total int64 `json:"total"` Items interface{} `json:"items"` } -// 业务错误码定义 const ( - // 成功 CodeSuccess = 0 - // 认证相关错误 10001-10099 - CodeLDAPAuthFailed = 10001 // LDAP 认证失败 - CodeInvalidCredentials = 10002 // 用户名或密码错误 - CodeTokenExpired = 10003 // Token 已过期 - CodeSSONotConfigured = 10004 // 子系统未配置 SSO - CodeSSOGenerateFailed = 10005 // SSO 跳转生成失败 + CodeLDAPAuthFailed = 10001 + CodeInvalidCredentials = 10002 + CodeTokenExpired = 10003 + CodeSSONotConfigured = 10004 + CodeSSOGenerateFailed = 10005 - // 任务相关错误 20001-20099 - CodeTaskCreateFailed = 20001 // Ansible 任务创建失败 - CodeTaskExecTimeout = 20002 // Ansible 任务执行超时 + CodeTaskCreateFailed = 20001 + CodeTaskExecTimeout = 20002 ) -// 错误码对应的默认消息 var codeMessages = map[int]string{ CodeSuccess: "success", CodeLDAPAuthFailed: "LDAP 认证失败", @@ -49,24 +42,21 @@ var codeMessages = map[int]string{ CodeTaskExecTimeout: "Ansible 任务执行超时", } -// GetMessage 根据错误码获取默认消息 -func GetMessage(code int) string { +func Message(code int) string { if msg, ok := codeMessages[code]; ok { return msg } return "未知错误" } -// Success 返回成功响应 func Success(c *gin.Context, data interface{}) { c.JSON(http.StatusOK, Response{ Code: CodeSuccess, - Message: GetMessage(CodeSuccess), + Message: Message(CodeSuccess), Data: data, }) } -// SuccessWithMessage 返回自定义消息的成功响应 func SuccessWithMessage(c *gin.Context, message string, data interface{}) { c.JSON(http.StatusOK, Response{ Code: CodeSuccess, @@ -75,11 +65,10 @@ func SuccessWithMessage(c *gin.Context, message string, data interface{}) { }) } -// SuccessWithPaginated 返回分页成功响应 func SuccessWithPaginated(c *gin.Context, total int64, items interface{}) { c.JSON(http.StatusOK, Response{ Code: CodeSuccess, - Message: GetMessage(CodeSuccess), + Message: Message(CodeSuccess), Data: PaginatedData{ Total: total, Items: items, @@ -87,17 +76,15 @@ func SuccessWithPaginated(c *gin.Context, total int64, items interface{}) { }) } -// Error 返回错误响应 -func Error(c *gin.Context, httpCode int, bizCode int, details string) { +func Error(c *gin.Context, httpCode, bizCode int, details string) { c.JSON(httpCode, Response{ Code: bizCode, - Message: GetMessage(bizCode), + Message: Message(bizCode), Details: details, }) } -// ErrorWithMessage 返回自定义消息的错误响应 -func ErrorWithMessage(c *gin.Context, httpCode int, bizCode int, message string, details string) { +func ErrorWithMessage(c *gin.Context, httpCode, bizCode int, message, details string) { c.JSON(httpCode, Response{ Code: bizCode, Message: message, @@ -105,17 +92,14 @@ func ErrorWithMessage(c *gin.Context, httpCode int, bizCode int, message string, }) } -// BadRequest 返回 400 错误 func BadRequest(c *gin.Context, details string) { Error(c, http.StatusBadRequest, CodeInvalidCredentials, details) } -// Unauthorized 返回 401 错误 func Unauthorized(c *gin.Context, details string) { Error(c, http.StatusUnauthorized, CodeTokenExpired, details) } -// Forbidden 返回 403 错误 func Forbidden(c *gin.Context, details string) { c.JSON(http.StatusForbidden, Response{ Code: http.StatusForbidden, @@ -124,7 +108,6 @@ func Forbidden(c *gin.Context, details string) { }) } -// NotFound 返回 404 错误 func NotFound(c *gin.Context, details string) { c.JSON(http.StatusNotFound, Response{ Code: http.StatusNotFound, @@ -133,7 +116,6 @@ func NotFound(c *gin.Context, details string) { }) } -// InternalError 返回 500 错误 func InternalError(c *gin.Context, details string) { c.JSON(http.StatusInternalServerError, Response{ Code: http.StatusInternalServerError, diff --git a/server/internal/router/.gitkeep b/server/internal/router/.gitkeep deleted file mode 100644 index 010eea4..0000000 --- a/server/internal/router/.gitkeep +++ /dev/null @@ -1,4 +0,0 @@ -# 路由注册模块 - -存放 API 路由配置: -- router.go: 路由注册,定义 API 路由表,绑定 Handler 和 Middleware diff --git a/authserver/internal/router/router.go b/server/internal/router/router.go similarity index 91% rename from authserver/internal/router/router.go rename to server/internal/router/router.go index 3390113..583edbf 100644 --- a/authserver/internal/router/router.go +++ b/server/internal/router/router.go @@ -1,9 +1,9 @@ package router import ( - "authserver/internal/config" - "authserver/internal/handler" - "authserver/internal/service" + "github.com/1024XEngineer/xinfra/server/internal/config" + "github.com/1024XEngineer/xinfra/server/internal/handler" + "github.com/1024XEngineer/xinfra/server/internal/service" "github.com/gin-gonic/gin" "gorm.io/gorm" diff --git a/server/internal/service/.gitkeep b/server/internal/service/.gitkeep deleted file mode 100644 index 4bb71a0..0000000 --- a/server/internal/service/.gitkeep +++ /dev/null @@ -1,8 +0,0 @@ -# 业务逻辑层 - -存放核心业务逻辑,编排 Repository: -- auth.go: 认证服务,LDAP 认证、本地降级认证、JWT 生成 -- ldap.go: LDAP 服务,封装 LDAP 连接、查询、认证逻辑 -- audit.go: 审计服务,记录登录审计、运维操作审计 -- subsystem.go: 子系统服务,子系统查询、OAuth 2.0 SSO URL 生成 -- ansible.go: Ansible 调度服务,创建任务、执行 Playbook、推送日志 diff --git a/authserver/internal/service/audit.go b/server/internal/service/audit.go similarity index 95% rename from authserver/internal/service/audit.go rename to server/internal/service/audit.go index 60fde17..4ffa8a7 100644 --- a/authserver/internal/service/audit.go +++ b/server/internal/service/audit.go @@ -3,7 +3,7 @@ package service import ( "encoding/json" - "authserver/internal/model" + "github.com/1024XEngineer/xinfra/server/internal/model" "gorm.io/gorm" ) diff --git a/authserver/internal/service/auth.go b/server/internal/service/auth.go similarity index 96% rename from authserver/internal/service/auth.go rename to server/internal/service/auth.go index 91b8f62..28085d2 100644 --- a/authserver/internal/service/auth.go +++ b/server/internal/service/auth.go @@ -8,10 +8,10 @@ import ( "strings" "time" - "authserver/internal/auth" - "authserver/internal/config" - "authserver/internal/model" - "authserver/internal/sso" + "github.com/1024XEngineer/xinfra/server/internal/auth" + "github.com/1024XEngineer/xinfra/server/internal/config" + "github.com/1024XEngineer/xinfra/server/internal/model" + "github.com/1024XEngineer/xinfra/server/internal/sso" "gorm.io/gorm" ) diff --git a/authserver/internal/service/wayen.go b/server/internal/service/wayen.go similarity index 97% rename from authserver/internal/service/wayen.go rename to server/internal/service/wayen.go index f370ab5..996ef6b 100644 --- a/authserver/internal/service/wayen.go +++ b/server/internal/service/wayen.go @@ -11,8 +11,8 @@ import ( "strings" "time" - "authserver/internal/config" - "authserver/internal/model" + "github.com/1024XEngineer/xinfra/server/internal/config" + "github.com/1024XEngineer/xinfra/server/internal/model" "gorm.io/gorm" ) diff --git a/authserver/internal/sso/login.go b/server/internal/sso/login.go similarity index 100% rename from authserver/internal/sso/login.go rename to server/internal/sso/login.go diff --git a/authserver/internal/sso/login_test.go b/server/internal/sso/login_test.go similarity index 100% rename from authserver/internal/sso/login_test.go rename to server/internal/sso/login_test.go diff --git a/authserver/internal/sso/metadata.go b/server/internal/sso/metadata.go similarity index 100% rename from authserver/internal/sso/metadata.go rename to server/internal/sso/metadata.go diff --git a/server/internal/websocket/.gitkeep b/server/internal/websocket/.gitkeep deleted file mode 100644 index 77150fc..0000000 --- a/server/internal/websocket/.gitkeep +++ /dev/null @@ -1,4 +0,0 @@ -# WebSocket 实时推送模块 - -存放 WebSocket 连接管理相关代码: -- hub.go: WebSocket Hub,管理客户端连接,广播 Ansible 任务日志 diff --git a/server/migrations/.gitkeep b/server/migrations/.gitkeep deleted file mode 100644 index 6c162a7..0000000 --- a/server/migrations/.gitkeep +++ /dev/null @@ -1,4 +0,0 @@ -# 数据库迁移脚本 - -存放按版本管理的表结构变更脚本: -- 001_init.sql: 初始化迁移,创建 users / subsystems / audit / ansible_tasks 表 diff --git a/server/pkg/database/.gitkeep b/server/pkg/database/.gitkeep deleted file mode 100644 index dc7614e..0000000 --- a/server/pkg/database/.gitkeep +++ /dev/null @@ -1,4 +0,0 @@ -# 数据库连接管理 - -存放数据库连接相关代码: -- mysql.go: MySQL 连接池,初始化连接、健康检查、优雅关闭 diff --git a/server/pkg/ldap/.gitkeep b/server/pkg/ldap/.gitkeep deleted file mode 100644 index c26fe50..0000000 --- a/server/pkg/ldap/.gitkeep +++ /dev/null @@ -1,4 +0,0 @@ -# LDAP 客户端封装 - -存放可复用的 LDAP 相关代码: -- client.go: LDAP 客户端,连接管理、用户认证、属性查询 diff --git a/server/pkg/sso/.gitkeep b/server/pkg/sso/.gitkeep deleted file mode 100644 index 42dd31e..0000000 --- a/server/pkg/sso/.gitkeep +++ /dev/null @@ -1,5 +0,0 @@ -# SSO 协议处理 - -存放 SSO 相关协议实现: -- oauth2.go: OAuth 2.0 客户端,主系统↔子系统的 Authorization Code 流程 -- saml.go: SAML 处理,主系统↔LDAP 的 SAML 断言生成和验证 diff --git a/authserver/web/.dockerignore b/server/web/.dockerignore similarity index 100% rename from authserver/web/.dockerignore rename to server/web/.dockerignore diff --git a/authserver/web/Dockerfile b/server/web/Dockerfile similarity index 100% rename from authserver/web/Dockerfile rename to server/web/Dockerfile diff --git a/authserver/web/index.html b/server/web/index.html similarity index 100% rename from authserver/web/index.html rename to server/web/index.html diff --git a/authserver/web/nginx.conf b/server/web/nginx.conf similarity index 100% rename from authserver/web/nginx.conf rename to server/web/nginx.conf diff --git a/authserver/web/package-lock.json b/server/web/package-lock.json similarity index 100% rename from authserver/web/package-lock.json rename to server/web/package-lock.json diff --git a/authserver/web/package.json b/server/web/package.json similarity index 100% rename from authserver/web/package.json rename to server/web/package.json diff --git a/authserver/web/src/App.vue b/server/web/src/App.vue similarity index 100% rename from authserver/web/src/App.vue rename to server/web/src/App.vue diff --git a/authserver/web/src/main.js b/server/web/src/main.js similarity index 100% rename from authserver/web/src/main.js rename to server/web/src/main.js diff --git a/authserver/web/src/style.css b/server/web/src/style.css similarity index 100% rename from authserver/web/src/style.css rename to server/web/src/style.css diff --git a/authserver/web/vite.config.js b/server/web/vite.config.js similarity index 100% rename from authserver/web/vite.config.js rename to server/web/vite.config.js From 81d2a8a60b7e42c4fb407958bad2f7568587b6f1 Mon Sep 17 00:00:00 2001 From: mac Date: Wed, 15 Jul 2026 14:47:42 +0800 Subject: [PATCH 13/13] refactor(frontend): merge authserver web login --- frontend/src/App.vue | 24 +- frontend/src/api/auth.ts | 65 ++-- frontend/src/api/request.ts | 3 +- frontend/src/api/subsystem.ts | 75 ++-- frontend/src/components/AuditLogTable.vue | 6 +- frontend/src/components/SubsystemCard.vue | 2 +- frontend/src/composables/useAuth.ts | 7 +- frontend/src/router/index.ts | 5 +- frontend/src/utils/auth.ts | 4 +- frontend/src/utils/sso.ts | 42 +++ frontend/src/views/auth/Login.vue | 80 +---- frontend/src/views/subsystem/Navigation.vue | 10 + frontend/vite.config.ts | 4 + server/web/.dockerignore | 4 - server/web/Dockerfile | 16 - server/web/index.html | 12 - server/web/nginx.conf | 90 ----- server/web/package-lock.json | 379 -------------------- server/web/package.json | 20 -- server/web/src/App.vue | 219 ----------- server/web/src/main.js | 5 - server/web/src/style.css | 206 ----------- server/web/vite.config.js | 15 - 23 files changed, 178 insertions(+), 1115 deletions(-) create mode 100644 frontend/src/utils/sso.ts delete mode 100644 server/web/.dockerignore delete mode 100644 server/web/Dockerfile delete mode 100644 server/web/index.html delete mode 100644 server/web/nginx.conf delete mode 100644 server/web/package-lock.json delete mode 100644 server/web/package.json delete mode 100644 server/web/src/App.vue delete mode 100644 server/web/src/main.js delete mode 100644 server/web/src/style.css delete mode 100644 server/web/vite.config.js diff --git a/frontend/src/App.vue b/frontend/src/App.vue index cb7f2e4..87418d1 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,7 +3,29 @@