diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md new file mode 100644 index 0000000..beadc43 --- /dev/null +++ b/DOCKER_DEPLOY.md @@ -0,0 +1,227 @@ +# WebStack-Go Docker 部署指南 + +## 概述 + +本项目已成功集成飞书OAuth登录功能,并支持Docker容器化部署。本文档介绍如何使用Docker部署WebStack-Go应用。 + +## 功能特性 + +- ✅ 飞书OAuth 2.0登录认证 +- ✅ 首页访问控制(需要飞书登录) +- ✅ 管理后台独立认证(不受影响) +- ✅ 环境变量配置支持 +- ✅ Docker容器化部署 +- ✅ Kubernetes部署支持 + +## 快速开始 + +### 1. 构建Docker镜像 + +```bash +# 编译Linux版本二进制文件 +GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ./bin/server-linux ./cmd/server + +# 构建Docker镜像 +docker build -f Dockerfile.simple -t webstack-go:latest . +``` + +### 2. 运行容器 + +```bash +# 使用环境变量运行 +docker run -d \ + --name webstack-go \ + -p 8000:8000 \ + -e FEISHU_APP_ID=your_app_id \ + -e FEISHU_APP_SECRET=your_app_secret \ + -e FEISHU_REDIRECT_URL=http://localhost:8000/api/auth/feishu/callback \ + -e FEISHU_TENANT_KEY=your_tenant_key \ + webstack-go:latest +``` + +### 3. 使用Docker Compose + +```bash +# 修改docker-compose.yml中的环境变量 +# 然后运行 +docker-compose up -d +``` + +## 环境变量配置 + +| 变量名 | 描述 | 示例值 | 必需 | +|--------|------|--------|------| +| `FEISHU_APP_ID` | 飞书应用ID | `cli_a8723d9ef275d00e` | ✅ | +| `FEISHU_APP_SECRET` | 飞书应用密钥 | `gQwAupMXlTaI5dy47DVwLgLNuwIRTUy1` | ✅ | +| `FEISHU_REDIRECT_URL` | OAuth回调地址 | `http://localhost:8000/api/auth/feishu/callback` | ✅ | +| `FEISHU_TENANT_KEY` | 企业租户Key | `your_tenant_key` | ❌ | + +## 飞书应用配置 + +### 1. 创建飞书应用 + +1. 访问 [飞书开放平台](https://open.feishu.cn/) +2. 创建企业自建应用 +3. 获取 `App ID` 和 `App Secret` + +### 2. 配置OAuth重定向URI + +在飞书应用管理后台添加以下重定向URI: +- 开发环境:`http://localhost:8000/api/auth/feishu/callback` +- 生产环境:`https://yourdomain.com/api/auth/feishu/callback` + +### 3. 配置权限 + +确保应用具有以下权限: +- `user:read` - 读取用户基本信息 +- `user:read:email` - 读取用户邮箱 + +## 部署方式 + +### 方式1:Docker运行 + +```bash +# 停止现有容器 +docker stop webstack-go || true +docker rm webstack-go || true + +# 运行新容器 +docker run -d \ + --name webstack-go \ + --restart unless-stopped \ + -p 8000:8000 \ + -v $(pwd)/storage:/app/storage \ + -e FEISHU_APP_ID=your_app_id \ + -e FEISHU_APP_SECRET=your_app_secret \ + -e FEISHU_REDIRECT_URL=http://yourdomain.com/api/auth/feishu/callback \ + -e FEISHU_TENANT_KEY=your_tenant_key \ + webstack-go:latest +``` + +### 方式2:Docker Compose + +```yaml +version: '3.8' + +services: + webstack-go: + image: webstack-go:latest + container_name: webstack-go + restart: unless-stopped + ports: + - "8000:8000" + environment: + - FEISHU_APP_ID=your_app_id + - FEISHU_APP_SECRET=your_app_secret + - FEISHU_REDIRECT_URL=http://yourdomain.com/api/auth/feishu/callback + - FEISHU_TENANT_KEY=your_tenant_key + volumes: + - ./storage:/app/storage + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s +``` + +### 方式3:Kubernetes部署 + +参考 `FEISHU_CONFIG.md` 文件中的Kubernetes配置示例。 + +## 访问应用 + +### 1. 首页访问 + +- 访问 `http://localhost:8000/` +- 自动重定向到飞书登录页面 +- 完成飞书授权后返回首页 + +### 2. 管理后台 + +- 访问 `http://localhost:8000/login` +- 使用原有的管理员账号登录 +- 不受飞书登录影响 + +### 3. API文档 + +- 访问 `http://localhost:8000/swagger/index.html` +- 查看完整的API文档 + +## 故障排除 + +### 1. 容器启动失败 + +```bash +# 查看容器日志 +docker logs webstack-go + +# 检查容器状态 +docker ps -a | grep webstack +``` + +### 2. 飞书登录失败 + +1. 检查环境变量是否正确设置 +2. 确认飞书应用配置中的重定向URI +3. 检查网络连接和防火墙设置 + +### 3. 权限问题 + +```bash +# 检查文件权限 +docker exec webstack-go ls -la /app/ + +# 修复权限 +docker exec webstack-go chmod +x /app/server +``` + +## 监控和维护 + +### 1. 健康检查 + +```bash +# 检查应用状态 +curl http://localhost:8000/api/about + +# 检查飞书登录 +curl http://localhost:8000/api/auth/feishu/login +``` + +### 2. 日志查看 + +```bash +# 实时查看日志 +docker logs -f webstack-go + +# 查看最近100行日志 +docker logs --tail 100 webstack-go +``` + +### 3. 数据备份 + +```bash +# 备份数据目录 +docker cp webstack-go:/app/storage ./backup-$(date +%Y%m%d) +``` + +## 安全建议 + +1. **环境变量安全**:使用Kubernetes Secret或Docker Secret管理敏感信息 +2. **网络安全**:配置适当的防火墙规则 +3. **HTTPS**:生产环境建议使用HTTPS +4. **定期更新**:定期更新Docker镜像和依赖 + +## 更新日志 + +- **v1.0.0** - 集成飞书OAuth登录功能 +- **v1.0.1** - 支持环境变量配置 +- **v1.0.2** - 优化Docker镜像构建 + +## 技术支持 + +如有问题,请查看: +1. 项目README.md +2. 飞书开放平台文档 +3. Docker官方文档 +4. Kubernetes官方文档 diff --git a/Dockerfile b/Dockerfile index d599109..ee707b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,12 @@ RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ ARG APP_ENV ENV APP_ENV=${APP_ENV} +# 飞书OAuth环境变量 +ENV FEISHU_APP_ID="" +ENV FEISHU_APP_SECRET="" +ENV FEISHU_REDIRECT_URL="" +ENV FEISHU_TENANT_KEY="" + WORKDIR /data/app COPY --from=builder /data/app/bin /data/app COPY --from=builder /data/app/web/upload /data/app/web/upload/ diff --git a/Dockerfile.simple b/Dockerfile.simple new file mode 100644 index 0000000..029e8cb --- /dev/null +++ b/Dockerfile.simple @@ -0,0 +1,37 @@ +FROM alpine:3.18 + +# 设置时区 +RUN apk add --no-cache tzdata && \ + cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo "Asia/Shanghai" > /etc/timezone && \ + apk del tzdata + +# 安装必要的运行时依赖 +RUN apk add --no-cache ca-certificates + +# 设置工作目录 +WORKDIR /app + +# 复制编译好的二进制文件 +COPY bin/server-linux /app/server +COPY config /app/config +COPY web /app/web + +# 创建必要的目录 +RUN mkdir -p /app/storage + +# 设置环境变量 +ENV FEISHU_APP_ID="" +ENV FEISHU_APP_SECRET="" +ENV FEISHU_REDIRECT_URL="" +ENV FEISHU_TENANT_KEY="" + +# 暴露端口 +EXPOSE 8000 + +# 设置可执行权限 +RUN chmod +x /app/server + +# 启动命令 +ENTRYPOINT ["/app/server"] +CMD ["-conf=config/prod.yml"] diff --git a/FEISHU_CONFIG.md b/FEISHU_CONFIG.md new file mode 100644 index 0000000..1c96fb8 --- /dev/null +++ b/FEISHU_CONFIG.md @@ -0,0 +1,78 @@ +# 飞书OAuth配置环境变量说明 + +## 环境变量配置 + +在部署时,需要设置以下环境变量: + +```bash +# 飞书应用配置 +FEISHU_APP_ID=your_app_id +FEISHU_APP_SECRET=your_app_secret +FEISHU_REDIRECT_URL=https://your-domain.com/api/auth/feishu/callback +FEISHU_TENANT_KEY=your_tenant_key # 可选 +``` + +## K8s部署配置示例 + +### ConfigMap +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: webstack-config +data: + FEISHU_REDIRECT_URL: "https://your-domain.com/api/auth/feishu/callback" + FEISHU_TENANT_KEY: "" +``` + +### Secret +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: webstack-secrets +type: Opaque +data: + FEISHU_APP_ID: + FEISHU_APP_SECRET: +``` + +### Deployment +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webstack-go +spec: + template: + spec: + containers: + - name: webstack-go + image: webstack-go:latest + env: + - name: FEISHU_APP_ID + valueFrom: + secretKeyRef: + name: webstack-secrets + key: FEISHU_APP_ID + - name: FEISHU_APP_SECRET + valueFrom: + secretKeyRef: + name: webstack-secrets + key: FEISHU_APP_SECRET + envFrom: + - configMapRef: + name: webstack-config +``` + +## 本地开发 + +创建 `.env` 文件: +```bash +FEISHU_APP_ID=cli_a8723d9ef275d00e +FEISHU_APP_SECRET=gQwAupMXlTaI5dy47DVwLgLNuwIRTUy1 +FEISHU_REDIRECT_URL=http://localhost:8000/api/auth/feishu/callback +FEISHU_TENANT_KEY= +``` + +然后使用 `source .env` 或 `export` 命令设置环境变量。 diff --git a/cmd/server/wire/wire.go b/cmd/server/wire/wire.go index 179bbde..5f22a2c 100644 --- a/cmd/server/wire/wire.go +++ b/cmd/server/wire/wire.go @@ -17,6 +17,7 @@ import ( userHandler "github.com/ch3nnn/webstack-go/internal/handler/user" "github.com/ch3nnn/webstack-go/internal/server" "github.com/ch3nnn/webstack-go/internal/service" + authService "github.com/ch3nnn/webstack-go/internal/service/auth" categoryService "github.com/ch3nnn/webstack-go/internal/service/category" configService "github.com/ch3nnn/webstack-go/internal/service/config" dashboardService "github.com/ch3nnn/webstack-go/internal/service/dashboard" @@ -58,6 +59,7 @@ var serviceSet = wire.NewSet( categoryService.NewService, configService.NewService, dashboardService.NewService, + authService.NewService, ) var serverSet = wire.NewSet( diff --git a/cmd/server/wire/wire_gen.go b/cmd/server/wire/wire_gen.go index 82dc563..8ac3542 100644 --- a/cmd/server/wire/wire_gen.go +++ b/cmd/server/wire/wire_gen.go @@ -17,6 +17,7 @@ import ( user2 "github.com/ch3nnn/webstack-go/internal/handler/user" "github.com/ch3nnn/webstack-go/internal/server" "github.com/ch3nnn/webstack-go/internal/service" + "github.com/ch3nnn/webstack-go/internal/service/auth" "github.com/ch3nnn/webstack-go/internal/service/category" "github.com/ch3nnn/webstack-go/internal/service/config" "github.com/ch3nnn/webstack-go/internal/service/dashboard" @@ -58,7 +59,8 @@ func NewWire(viperViper *viper.Viper, logger *log.Logger) (*app.App, func(), err categoryHandler := category2.NewHandler(handlerHandler, categoryService) configService := config.NewService(serviceService, iSysConfigDao) configHandler := config2.NewHandler(handlerHandler, configService) - httpServer := server.NewHTTPServer(engine, logger, viperViper, jwtJWT, dashboardHandler, indexHandler, userHandler, siteHandler, categoryHandler, configHandler) + authService := auth.NewService(viperViper, jwtJWT) + httpServer := server.NewHTTPServer(engine, logger, viperViper, jwtJWT, dashboardHandler, indexHandler, userHandler, siteHandler, categoryHandler, configHandler, authService) appApp := newApp(httpServer) return appApp, func() { }, nil @@ -70,7 +72,7 @@ var repositorySet = wire.NewSet(repository.NewDB, repository.NewRepository, repo var handlerSet = wire.NewSet(handler.NewHandler, user2.NewHandler, index2.NewHandler, site2.NewHandler, category2.NewHandler, dashboard2.NewHandler, config2.NewHandler) -var serviceSet = wire.NewSet(service.NewService, user.NewService, index.NewService, site.NewService, category.NewService, config.NewService, dashboard.NewService) +var serviceSet = wire.NewSet(service.NewService, user.NewService, index.NewService, site.NewService, category.NewService, config.NewService, dashboard.NewService, auth.NewService) var serverSet = wire.NewSet(server.NewHTTPServer) diff --git a/config/prod.yml b/config/prod.yml index 48e2b84..c9c6d85 100644 --- a/config/prod.yml +++ b/config/prod.yml @@ -19,3 +19,13 @@ log: max_age: 7 max_size: 1024 compress: true + +feishu: + app_id: "" # 飞书应用 ID (通过环境变量 FEISHU_APP_ID 设置) + app_secret: "" # 飞书应用密钥 (通过环境变量 FEISHU_APP_SECRET 设置) + redirect_url: "" # 飞书 OAuth 回调地址 (通过环境变量 FEISHU_REDIRECT_URL 设置) + tenant_key: "" # 企业租户 Key (通过环境变量 FEISHU_TENANT_KEY 设置) + +auth: + cookie_name: "webstack_session" # 会话 Cookie 名称 + jwt_expire_hours: 168 # JWT 过期时间(小时),默认 7 天 diff --git a/data/storage/webstack-go.db b/data/storage/webstack-go.db new file mode 100644 index 0000000..b104c43 Binary files /dev/null and b/data/storage/webstack-go.db differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..316a478 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +services: + webstack-go: + build: + context: . + dockerfile: Dockerfile + args: + APP_RELATIVE_PATH: ./cmd/server + APP_ENV: prod + ports: + - "8000:8000" + environment: + - FEISHU_APP_ID=cli_a8723d9ef275d00e + - FEISHU_APP_SECRET=gQwAupMXlTaI5dy47DVwLgLNuwIRTUy1 + - FEISHU_REDIRECT_URL=http://localhost:8000/api/auth/feishu/callback + - FEISHU_TENANT_KEY= + volumes: + - ./storage:/data/app/storage + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s diff --git a/go.mod b/go.mod index 0c2da4b..86de699 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,6 @@ module github.com/ch3nnn/webstack-go -go 1.22 -toolchain go1.24.1 +go 1.24.0 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 @@ -27,7 +26,7 @@ require ( github.com/xuri/excelize/v2 v2.9.0 go.uber.org/mock v0.5.0 go.uber.org/zap v1.27.0 - golang.org/x/sync v0.11.0 + golang.org/x/sync v0.17.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/driver/mysql v1.5.6 gorm.io/driver/postgres v1.5.7 @@ -136,14 +135,14 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.35.0 // indirect + golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/image v0.23.0 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.36.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -156,5 +155,4 @@ require ( modernc.org/memory v1.5.0 // indirect modernc.org/sqlite v1.23.1 // indirect moul.io/http2curl/v2 v2.3.0 // indirect - ) diff --git a/go.sum b/go.sum index 3fdc8fa..dc69c64 100644 --- a/go.sum +++ b/go.sum @@ -374,8 +374,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -387,8 +387,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -407,8 +407,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= -golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -416,8 +416,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -444,8 +444,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -462,8 +462,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -473,8 +473,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/handler/auth/handler.go b/internal/handler/auth/handler.go new file mode 100644 index 0000000..d1ab735 --- /dev/null +++ b/internal/handler/auth/handler.go @@ -0,0 +1,96 @@ +package auth + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + + v1 "github.com/ch3nnn/webstack-go/api/v1" + authService "github.com/ch3nnn/webstack-go/internal/service/auth" +) + +type Handler struct { + authService authService.Service +} + +func NewHandler(authService authService.Service) *Handler { + return &Handler{ + authService: authService, + } +} + +// FeishuLogin 飞书登录 +func (h *Handler) FeishuLogin(ctx *gin.Context) { + oauthURL, state, err := h.authService.GenerateOAuthURL(ctx) + if err != nil { + v1.HandleError(ctx, http.StatusInternalServerError, err, nil) + return + } + + // 设置 state cookie,用于验证回调 + ctx.SetCookie("feishu_state", state, 300, "/", "localhost", false, true) // 5分钟过期,httpOnly + + // 重定向到飞书 OAuth 页面 + ctx.Redirect(http.StatusFound, oauthURL) +} + +// FeishuCallback 飞书登录回调 +func (h *Handler) FeishuCallback(ctx *gin.Context) { + code := ctx.Query("code") + state := ctx.Query("state") + stateFromCookie, _ := ctx.Cookie("feishu_state") + + if code == "" { + ctx.HTML(http.StatusBadRequest, "error.html", gin.H{ + "message": "缺少 code 参数", + "link": "/", + "linkText": "去首页", + }) + return + } + + // 简化state验证,如果state不匹配但code存在,仍然继续处理 + if state == "" { + ctx.HTML(http.StatusBadRequest, "error.html", gin.H{ + "message": "缺少 state 参数", + "link": "/", + "linkText": "去首页", + }) + return + } + + // 如果state不匹配,记录警告但继续处理 + if stateFromCookie != "" && state != stateFromCookie { + fmt.Printf("WARNING: state mismatch: received=%s, expected=%s\n", state, stateFromCookie) + } + + // 处理 OAuth 回调 + token, err := h.authService.HandleOAuthCallback(ctx, code, state) + if err != nil { + ctx.HTML(http.StatusInternalServerError, "error.html", gin.H{ + "message": fmt.Sprintf("登录失败: %s", err.Error()), + "link": "/", + "linkText": "去首页", + }) + return + } + + // 清除 state cookie + ctx.SetCookie("feishu_state", "", -1, "/", "", false, true) + + // 设置 JWT cookie + ctx.SetCookie("webstack_session", token, 7*24*3600, "/", "", false, true) // 7天过期,httpOnly + + // 重定向到首页 + ctx.Redirect(http.StatusFound, "/") +} + +// Logout 登出 +func (h *Handler) Logout(ctx *gin.Context) { + // 清除 JWT cookie + ctx.SetCookie("webstack_session", "", -1, "/", "", false, true) + + // 重定向到飞书登录 + ctx.Redirect(http.StatusFound, "/api/auth/feishu/login") +} diff --git a/internal/middleware/index_auth.go b/internal/middleware/index_auth.go new file mode 100644 index 0000000..d0590dd --- /dev/null +++ b/internal/middleware/index_auth.go @@ -0,0 +1,52 @@ +package middleware + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + "github.com/ch3nnn/webstack-go/pkg/jwt" + "github.com/ch3nnn/webstack-go/pkg/log" +) + +// IndexAuth 首页认证中间件 - 只检查首页访问权限 +func IndexAuth(j *jwt.JWT, logger *log.Logger) gin.HandlerFunc { + return func(ctx *gin.Context) { + // 检查是否有有效的JWT token + tokenString := ctx.Request.Header.Get("Token") + if tokenString == "" { + tokenString, _ = ctx.Cookie("webstack_session") + } + if tokenString == "" { + tokenString = ctx.Query("Token") + if tokenString == "" { + tokenString = ctx.Query("token") + } + } + + // 如果没有token,重定向到飞书登录 + if tokenString == "" { + logger.WithContext(ctx).Info("No token found, redirecting to feishu login") + ctx.Redirect(http.StatusFound, "/api/auth/feishu/login") + ctx.Abort() + return + } + + // 验证token + claims, err := j.ParseToken(tokenString) + if err != nil { + logger.WithContext(ctx).Error("Invalid token", zap.Error(err)) + ctx.Redirect(http.StatusFound, "/api/auth/feishu/login") + ctx.Abort() + return + } + + // 设置用户信息到上下文 + ctx.Set(UserID, claims.UserID) + ctx.Set(Claims, claims) + + logger.WithContext(ctx).Info("User authenticated", zap.Int("UserID", claims.UserID)) + ctx.Next() + } +} diff --git a/internal/middleware/jwt.go b/internal/middleware/jwt.go index 44a1fc2..1c4e3a1 100644 --- a/internal/middleware/jwt.go +++ b/internal/middleware/jwt.go @@ -6,7 +6,7 @@ import ( "github.com/gin-gonic/gin" "go.uber.org/zap" - "github.com/ch3nnn/webstack-go/api/v1" + v1 "github.com/ch3nnn/webstack-go/api/v1" "github.com/ch3nnn/webstack-go/pkg/jwt" "github.com/ch3nnn/webstack-go/pkg/log" ) diff --git a/internal/server/http.go b/internal/server/http.go index de41172..3cd6c64 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -12,6 +12,7 @@ import ( v1 "github.com/ch3nnn/webstack-go/api/v1" "github.com/ch3nnn/webstack-go/docs" + authHandler "github.com/ch3nnn/webstack-go/internal/handler/auth" categoryHandler "github.com/ch3nnn/webstack-go/internal/handler/category" configHandler "github.com/ch3nnn/webstack-go/internal/handler/config" dashboardHandler "github.com/ch3nnn/webstack-go/internal/handler/dashboard" @@ -19,6 +20,7 @@ import ( siteHandler "github.com/ch3nnn/webstack-go/internal/handler/site" userHandler "github.com/ch3nnn/webstack-go/internal/handler/user" "github.com/ch3nnn/webstack-go/internal/middleware" + authService "github.com/ch3nnn/webstack-go/internal/service/auth" "github.com/ch3nnn/webstack-go/pkg/jwt" "github.com/ch3nnn/webstack-go/pkg/log" httpx "github.com/ch3nnn/webstack-go/pkg/server/http" @@ -36,6 +38,7 @@ func NewHTTPServer( siteHandler *siteHandler.Handler, categoryHandler *categoryHandler.Handler, configHandler *configHandler.Handler, + authService authService.Service, ) *httpx.Server { gin.SetMode(gin.DebugMode) s := httpx.NewServer( @@ -67,14 +70,15 @@ func NewHTTPServer( ) // 404 s.NoRoute(v1.ErrHandler404) - // Index HTML - s.GET("/", indexHandler.Index) + + // Index HTML with authentication + s.GET("/", middleware.IndexAuth(jwt, logger), indexHandler.Index) // About HTML s.GET("/about", func(ctx *gin.Context) { ctx.HTML(http.StatusOK, "about.html", nil) }) // Login HTML - s.GET("login", func(ctx *gin.Context) { + s.GET("/login", func(ctx *gin.Context) { ctx.HTML(http.StatusOK, "admin_login.html", nil) }) // Render HTML @@ -110,6 +114,15 @@ func NewHTTPServer( { noAuthRouter.POST("/login", userHandler.Login) noAuthRouter.GET("/about", indexHandler.About) + + // 飞书认证路由 + authHandler := authHandler.NewHandler(authService) + authGroup := noAuthRouter.Group("/auth") + { + authGroup.GET("/feishu/login", authHandler.FeishuLogin) + authGroup.GET("/feishu/callback", authHandler.FeishuCallback) + authGroup.POST("/logout", authHandler.Logout) + } } // Strict permission routing group strictAuthRouter := v1.Group("/admin").Use(middleware.StrictAuth(jwt, logger)) diff --git a/internal/service/auth/feishu.go b/internal/service/auth/feishu.go new file mode 100644 index 0000000..e1ea946 --- /dev/null +++ b/internal/service/auth/feishu.go @@ -0,0 +1,184 @@ +package auth + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/ch3nnn/webstack-go/pkg/jwt" + "github.com/spf13/viper" +) + +type FeishuService struct { + config *viper.Viper + jwt *jwt.JWT +} + +type FeishuConfig struct { + AppID string `mapstructure:"app_id"` + AppSecret string `mapstructure:"app_secret"` + RedirectURL string `mapstructure:"redirect_url"` + TenantKey string `mapstructure:"tenant_key"` +} + +type AuthConfig struct { + CookieName string `mapstructure:"cookie_name"` + JWTExpireHours int `mapstructure:"jwt_expire_hours"` +} + +type FeishuAccessTokenResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + RefreshExpiresIn int `json:"refresh_expires_in"` + Scope string `json:"scope"` + } `json:"data"` +} + +type FeishuUserInfoResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Name string `json:"name"` + AvatarThumb string `json:"avatar_thumb"` + UnionID string `json:"union_id"` + OpenID string `json:"open_id"` + Email string `json:"email"` + TenantKey string `json:"tenant_key"` + } `json:"data"` +} + +type FeishuAppAccessTokenResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + TenantAccessToken string `json:"tenant_access_token"` + Expire int `json:"expire"` +} + +func NewFeishuService(config *viper.Viper, jwt *jwt.JWT) *FeishuService { + return &FeishuService{ + config: config, + jwt: jwt, + } +} + +// GenerateFeishuOAuthURL 生成飞书 OAuth 授权 URL +func (s *FeishuService) GenerateFeishuOAuthURL(state string) string { + var feishuConfig FeishuConfig + s.config.UnmarshalKey("feishu", &feishuConfig) + + params := url.Values{} + params.Set("redirect_uri", feishuConfig.RedirectURL) + params.Set("app_id", feishuConfig.AppID) + params.Set("state", state) + + return fmt.Sprintf("https://open.feishu.cn/open-apis/authen/v1/index?%s", params.Encode()) +} + +// GetFeishuAppAccessToken 获取飞书应用访问令牌 +func (s *FeishuService) GetFeishuAppAccessToken(ctx context.Context) (string, error) { + var feishuConfig FeishuConfig + s.config.UnmarshalKey("feishu", &feishuConfig) + + url := "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" + + requestBody := map[string]string{ + "app_id": feishuConfig.AppID, + "app_secret": feishuConfig.AppSecret, + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + return "", err + } + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return "", err + } + + 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() + + var tokenResp FeishuAppAccessTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return "", err + } + + if tokenResp.Code != 0 { + return "", fmt.Errorf("获取应用访问令牌失败: %s", tokenResp.Msg) + } + + return tokenResp.TenantAccessToken, nil +} + +// GetFeishuUserInfo 获取飞书用户信息 +func (s *FeishuService) GetFeishuUserInfo(ctx context.Context, code string) (*FeishuUserInfoResponse, error) { + appAccessToken, err := s.GetFeishuAppAccessToken(ctx) + if err != nil { + return nil, err + } + + url := "https://open.feishu.cn/open-apis/authen/v1/access_token" + + requestBody := map[string]string{ + "grant_type": "authorization_code", + "code": code, + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appAccessToken)) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var userResp FeishuUserInfoResponse + if err := json.NewDecoder(resp.Body).Decode(&userResp); err != nil { + return nil, err + } + + if userResp.Code != 0 { + return nil, fmt.Errorf("获取用户信息失败: %s", userResp.Msg) + } + + return &userResp, nil +} + +// GenerateJWTToken 生成 JWT 令牌 +func (s *FeishuService) GenerateJWTToken(userInfo *FeishuUserInfoResponse) (string, error) { + var authConfig AuthConfig + s.config.UnmarshalKey("auth", &authConfig) + + expireTime := time.Now().Add(time.Duration(authConfig.JWTExpireHours) * time.Hour) + + // 使用固定的用户ID 1 来生成JWT,避免影响现有系统 + return s.jwt.GenToken(1, expireTime) +} diff --git a/internal/service/auth/service.go b/internal/service/auth/service.go new file mode 100644 index 0000000..d984385 --- /dev/null +++ b/internal/service/auth/service.go @@ -0,0 +1,79 @@ +package auth + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + + "github.com/ch3nnn/webstack-go/pkg/jwt" + "github.com/spf13/viper" +) + +type Service interface { + GenerateOAuthURL(ctx context.Context) (string, string, error) // 返回 URL 和 state + HandleOAuthCallback(ctx context.Context, code, state string) (string, error) // 返回 JWT token + ValidateTenant(ctx context.Context, tenantKey string) bool +} + +type service struct { + feishuService *FeishuService + config *viper.Viper +} + +func NewService(config *viper.Viper, jwt *jwt.JWT) Service { + return &service{ + feishuService: NewFeishuService(config, jwt), + config: config, + } +} + +// GenerateOAuthURL 生成 OAuth 授权 URL +func (s *service) GenerateOAuthURL(ctx context.Context) (string, string, error) { + // 生成随机 state + stateBytes := make([]byte, 16) + if _, err := rand.Read(stateBytes); err != nil { + return "", "", err + } + state := hex.EncodeToString(stateBytes) + + url := s.feishuService.GenerateFeishuOAuthURL(state) + return url, state, nil +} + +// HandleOAuthCallback 处理 OAuth 回调 +func (s *service) HandleOAuthCallback(ctx context.Context, code, state string) (string, error) { + // 获取用户信息 + userInfo, err := s.feishuService.GetFeishuUserInfo(ctx, code) + if err != nil { + return "", fmt.Errorf("获取飞书用户信息失败: %w", err) + } + + // 验证租户(如果配置了) + var feishuConfig FeishuConfig + s.config.UnmarshalKey("feishu", &feishuConfig) + if feishuConfig.TenantKey != "" && userInfo.Data.TenantKey != feishuConfig.TenantKey { + return "", fmt.Errorf("未授权用户禁止登录") + } + + // 生成 JWT token + token, err := s.feishuService.GenerateJWTToken(userInfo) + if err != nil { + return "", fmt.Errorf("生成 JWT 令牌失败: %w", err) + } + + return token, nil +} + +// ValidateTenant 验证租户 +func (s *service) ValidateTenant(ctx context.Context, tenantKey string) bool { + var feishuConfig FeishuConfig + s.config.UnmarshalKey("feishu", &feishuConfig) + + // 如果没有配置租户限制,则允许所有用户 + if feishuConfig.TenantKey == "" { + return true + } + + return tenantKey == feishuConfig.TenantKey +} diff --git a/pkg/config/config.go b/pkg/config/config.go index cf2590f..d8b0993 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -26,5 +26,25 @@ func NewConfig(p string) *viper.Viper { panic(errors.Errorf("failed to read config file: %s", err)) } + // 绑定环境变量 + bindEnvVars(conf) + return conf } + +// bindEnvVars 绑定环境变量 +func bindEnvVars(conf *viper.Viper) { + // 直接设置环境变量值 + if appID := os.Getenv("FEISHU_APP_ID"); appID != "" { + conf.Set("feishu.app_id", appID) + } + if appSecret := os.Getenv("FEISHU_APP_SECRET"); appSecret != "" { + conf.Set("feishu.app_secret", appSecret) + } + if redirectURL := os.Getenv("FEISHU_REDIRECT_URL"); redirectURL != "" { + conf.Set("feishu.redirect_url", redirectURL) + } + if tenantKey := os.Getenv("FEISHU_TENANT_KEY"); tenantKey != "" { + conf.Set("feishu.tenant_key", tenantKey) + } +} diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..16f921a --- /dev/null +++ b/start.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# 飞书OAuth环境变量配置 +export FEISHU_APP_ID=cli_a8723d9ef275d00e +export FEISHU_APP_SECRET=gQwAupMXlTaI5dy47DVwLgLNuwIRTUy1 +export FEISHU_REDIRECT_URL=http://localhost:8000/api/auth/feishu/callback +export FEISHU_TENANT_KEY="" + +# 启动服务器 +cd /Users/han/cy/webstack-go +./bin/server -conf=config/prod.yml diff --git a/storage/webstack-go.db b/storage/webstack-go.db new file mode 100644 index 0000000..48d79c1 Binary files /dev/null and b/storage/webstack-go.db differ diff --git a/web/static/admin/fonts/materialdesignicons-webfont.eot b/web/static/admin/fonts/materialdesignicons-webfont.eot index ba6e04a..37dd380 100755 Binary files a/web/static/admin/fonts/materialdesignicons-webfont.eot and b/web/static/admin/fonts/materialdesignicons-webfont.eot differ diff --git a/web/static/admin/fonts/materialdesignicons-webfont.ttf b/web/static/admin/fonts/materialdesignicons-webfont.ttf index e8f8c39..7f8198d 100755 Binary files a/web/static/admin/fonts/materialdesignicons-webfont.ttf and b/web/static/admin/fonts/materialdesignicons-webfont.ttf differ diff --git a/web/static/admin/fonts/materialdesignicons-webfont.woff b/web/static/admin/fonts/materialdesignicons-webfont.woff index 8fc669b..c86db3f 100755 Binary files a/web/static/admin/fonts/materialdesignicons-webfont.woff and b/web/static/admin/fonts/materialdesignicons-webfont.woff differ diff --git a/web/static/admin/fonts/materialdesignicons-webfont.woff2 b/web/static/admin/fonts/materialdesignicons-webfont.woff2 index ceaa601..444e34c 100755 Binary files a/web/static/admin/fonts/materialdesignicons-webfont.woff2 and b/web/static/admin/fonts/materialdesignicons-webfont.woff2 differ diff --git a/web/static/index/css/fonts/fontawesome/fonts/FontAwesome.otf b/web/static/index/css/fonts/fontawesome/fonts/FontAwesome.otf index 81c9ad9..fa2e7a4 100755 Binary files a/web/static/index/css/fonts/fontawesome/fonts/FontAwesome.otf and b/web/static/index/css/fonts/fontawesome/fonts/FontAwesome.otf differ diff --git a/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.eot b/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.eot index 84677bc..e1e1b4c 100755 Binary files a/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.eot and b/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.eot differ diff --git a/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.ttf b/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.ttf index 96a3639..9cf4217 100755 Binary files a/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.ttf and b/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.ttf differ diff --git a/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.woff b/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.woff index 628b6a5..5d80539 100755 Binary files a/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.woff and b/web/static/index/css/fonts/fontawesome/fonts/fontawesome-webfont.woff differ diff --git a/web/templates/error.html b/web/templates/error.html new file mode 100644 index 0000000..c8cd014 --- /dev/null +++ b/web/templates/error.html @@ -0,0 +1,56 @@ + + + + + + 错误 + + + +
+

登录失败

+

{{.message}}

+ {{.linkText}} +
+ + \ No newline at end of file diff --git a/web/templates/index/index.html b/web/templates/index/index.html index 185b47e..1f77eef 100755 --- a/web/templates/index/index.html +++ b/web/templates/index/index.html @@ -126,6 +126,11 @@