[优化] 更新认证机制,使用 OPENFLARE_TOKEN 替代 Bearer Token

This commit is contained in:
ryan
2026-06-04 11:10:06 +08:00
parent bdc96f6d8e
commit 6aa71a4da8
38 changed files with 462 additions and 387 deletions
+1 -1
View File
@@ -91,7 +91,7 @@ docker run -d \
启动参数说明:
* **`-p 3000:3000`**:映射宿主机 `3000` 端口到容器内 `3000` 端口。
* **`-v $(pwd)/openflare-data:/data`**:挂载本地目录到容器的 `/data`,确保数据库文件 `openflare.db` 在重启或重建容器时不丢失。
* **`SESSION_SECRET`**:必须配置的 Session 密钥签名哈希。
* **`SESSION_SECRET`**:建议配置的临时 Session 签名密钥,主要用于 OAuth 状态等非管理端 API 鉴权流程;管理端 API 登录凭证通过 `OPENFLARE_TOKEN` 请求头传递。
---
+1 -1
View File
@@ -79,7 +79,7 @@ Agent 本地 Pages 部署目录
* Gin 提供 HTTP 服务。
* GORM 访问 SQLite 或 PostgreSQL。
* 现有登录体系提供管理端 Session。
* 现有登录体系签发管理端用户 Token,管理端 API 通过 `OPENFLARE_TOKEN` 请求头鉴权。
* 认证源与外部账号绑定支持 GitHub OAuth 和标准 OIDC。
* Go Server 托管 `openflare_server/web` 静态构建产物。
+3 -3
View File
@@ -9,7 +9,7 @@
| 现象 | 先看哪里 |
| --- | --- |
| 管理端打不开 | Server 容器或进程日志、端口监听 |
| 登录异常 | 默认账号、Session Secret、浏览器请求、Server 日志 |
| 登录异常 | 默认账号、OPENFLARE_TOKEN、浏览器请求、Server 日志 |
| 数据无法保存 | 数据库连接、SQLite 文件权限、PostgreSQL 健康状态 |
| Agent 离线 | Agent 日志、Token、Server 地址、网络连通性 |
| 发布后节点未更新 | 激活版本、节点 heartbeat、应用记录 |
@@ -85,8 +85,8 @@ NEXT_DEV_BACKEND_URL=http://127.0.0.1:3000 pnpm dev
1. 确认连接的是预期数据库,避免 `SQLITE_PATH` 或 `DSN` 指向了另一个环境。
2. 查看 Server 日志中使用的是 `sqlite` 还是 `postgres`。
3. 如果部署在多副本或反向代理后,确认 `SESSION_SECRET` 固定且各实例一致。
4. 清理浏览器 Cookie 后重新登录。
3. 在浏览器开发者工具中确认管理端 API 请求携带 `OPENFLARE_TOKEN` 请求头。
4. 清理浏览器本地存储中的旧 `openflare_token` 后重新登录。
### 应急重置管理员密码
+4 -4
View File
@@ -167,19 +167,19 @@ v1-v7 视为历史初始基线,不再维护逐版本升级文件。v8-v17 是
- Client 心跳返回 tunnel 配置版本摘要。
- Client 可拉取完整配置(relay 列表 + frpc 代理定义)。
- Client 上报配置应用结果。
* **Admin Tunnel 管理 API** - `/api/tunnels/*`,要求 Admin Session。
* **Admin Tunnel 管理 API** - `/api/tunnels/*`,要求管理端 `OPENFLARE_TOKEN`。
- CRUD tunnel 实体(创建、查询、更新、删除)。
- Token 管理(生成、轮换)。
- 强制同步(触发 Client 立即拉取新配置)。
* **Admin Pages 管理 API** - `/api/pages/*`,要求 Admin Session。
* **Admin Pages 管理 API** - `/api/pages/*`,要求管理端 `OPENFLARE_TOKEN`。
- CRUD Pages 项目,包括 SPA fallback 启用状态与回退路径。
- 上传 zip 部署包、查看部署历史、激活部署、删除非激活部署。
* **Agent Pages 下载 API** - `/api/agent/pages/*`,使用 `X-Agent-Token` 认证。
- Agent 仅能按激活配置引用的部署 ID 拉取静态部署包,不提供任意文件读取或远程命令入口。
* 总览与节点详情优先使用专用聚合接口。
* 管理端变更类接口统一使用 `POST`;只读接口使用 `GET`。
* 管理端继续复用现有登录、角色与 Session。
* 第三方登录统一通过认证源 API 进入,认证源管理接口必须要求 Root Session。
* 管理端登录成功后返回用户 token;管理端 API 只允许从 `OPENFLARE_TOKEN` 请求头读取登录凭证,不得通过 Cookie Session 放行。
* 第三方登录统一通过认证源 API 进入,认证源管理接口必须要求 Root 级 `OPENFLARE_TOKEN`。
* `/api/status` 只能返回已启用认证源的公开字段,不得返回 Client Secret。
* 第三方账号未绑定且注册关闭时,应提供绑定已有账号流程,不得自动创建用户。
* Agent/Relay/Client 正式请求统一使用对应的专属 token(`agent_token` / `relay_token`(即 agent_token) / `tunnel_token`)。
+9 -3
View File
@@ -20,7 +20,7 @@ OpenFlare 的管理端 API 与 Agent API 都使用 JSON。
| 类型 | 约定 |
| --- | --- |
| 管理端 API | 由管理端 Session 鉴权 |
| 管理端 API | 由 `OPENFLARE_TOKEN` 请求头鉴权 |
| Agent API | 固定放在 `/api/agent/*` |
| Relay API | 固定放在 `/api/relay/*`,使用 `X-Agent-Token` 鉴权(与 Agent 复用同一 token) |
| OpenFlared API | 固定放在 `/api/flared/*`,使用 `X-Tunnel-Token` 鉴权(独立的 tunnel_token) |
@@ -29,7 +29,7 @@ OpenFlare 的管理端 API 与 Agent API 都使用 JSON。
## WAF IP 组接口
管理端 WAF IP 组接口统一要求管理端 Session 鉴权:
管理端 WAF IP 组接口统一要求管理端 `OPENFLARE_TOKEN` 鉴权:
| 方法 | 路径 | 说明 |
| --- | --- | --- |
@@ -47,7 +47,13 @@ IP 组 `type` 支持 `manual`、`automatic`、`subscription`。自动型 IP 组
## 鉴权
管理端继续复用现有登录、角色与 Session。
管理端登录成功后返回用户 token,后续所有管理端 API 必须在请求头中携带:
```http
OPENFLARE_TOKEN: <token>
```
Server 只从 `OPENFLARE_TOKEN` 读取管理端登录凭证,不再通过 Cookie Session 放行管理端 API。角色和用户状态仍以数据库中的当前用户记录为准。
Agent 正式请求统一使用节点专属 `agent_token`,首次接入可使用全局 `discovery_token`。Agent 请求头固定为:
+2 -2
View File
@@ -64,7 +64,7 @@ go run . --port 3000 --log-dir ./logs
| `PORT` | Server 监听端口 | `3000` |
| `GIN_MODE` | Gin 运行模式 | 非 `debug` 时按 release |
| `LOG_LEVEL` | 日志等级 | `info` |
| `SESSION_SECRET` | Session 签名密钥 | 启动时随机生成 |
| `SESSION_SECRET` | 临时 Session 签名密钥,主要用于 OAuth 状态等非管理端 API 鉴权流程 | 启动时随机生成 |
| `SQLITE_PATH` | SQLite 数据库文件路径 | `openflare.db` |
| `DSN` | PostgreSQL DSN,设置后优先于 SQLite | 空 |
| `SQL_DSN` | 兼容旧命名的 PostgreSQL DSN,优先级低于 `DSN` | 空 |
@@ -76,7 +76,7 @@ go run . --port 3000 --log-dir ./logs
* `DSN` 与 `SQL_DSN` 同时存在时优先使用 `DSN`。
* `DSN` 或 `SQL_DSN` 与 `SQLITE_PATH` 同时存在时优先使用 PostgreSQL。
* 当目标 PostgreSQL 数据库为空且本地 `SQLITE_PATH` 文件存在时,Server 启动阶段会自动迁移 SQLite 数据,并在日志中输出按表迁移进度。
* `SESSION_SECRET` 生产环境必须显式配置。
* `SESSION_SECRET` 生产环境建议显式配置,避免 OAuth 授权状态等临时会话在重启后失效;管理端 API 登录凭证不再通过 Cookie Session 传递,而是使用 `OPENFLARE_TOKEN` 请求头。
* `REDIS_CONN_STRING` 未配置时,相关能力回退为进程内实现。
## 运行时 Option
+6 -6
View File
@@ -11,7 +11,7 @@ import (
// @Summary List access logs
// @Tags AccessLogs
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param node_id query string false "Node ID"
// @Param remote_addr query string false "Remote address"
// @Param host query string false "Host"
@@ -35,7 +35,7 @@ func GetAccessLogs(c *gin.Context) {
// @Summary List folded access logs
// @Tags AccessLogs
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param node_id query string false "Node ID"
// @Param remote_addr query string false "Remote address"
// @Param host query string false "Host"
@@ -62,7 +62,7 @@ func GetFoldedAccessLogs(c *gin.Context) {
// @Summary List folded access log IP summaries
// @Tags AccessLogs
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param node_id query string false "Node ID"
// @Param remote_addr query string false "Remote address"
// @Param host query string false "Host"
@@ -99,7 +99,7 @@ func GetFoldedAccessLogIPs(c *gin.Context) {
// @Summary List access log IP summaries
// @Tags AccessLogs
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param node_id query string false "Node ID"
// @Param remote_addr query string false "Remote address"
// @Param host query string false "Host"
@@ -130,7 +130,7 @@ func GetAccessLogIPSummaries(c *gin.Context) {
// @Summary Get access log IP trend
// @Tags AccessLogs
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param node_id query string false "Node ID"
// @Param remote_addr query string true "Remote address"
// @Param host query string false "Host"
@@ -158,7 +158,7 @@ func GetAccessLogIPTrend(c *gin.Context) {
// @Tags AccessLogs
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/access-logs/cleanup [post]
func CleanupAccessLogs(c *gin.Context) {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
// @Summary Get default ACME account
// @Tags AcmeAccounts
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/acme-accounts/default [get]
func GetDefaultAcmeAccount(c *gin.Context) {
+3 -3
View File
@@ -296,7 +296,7 @@ func handleAgentWSStatus(c *gin.Context, node *model.Node, message service.Agent
// @Summary List nodes
// @Tags Nodes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/nodes/ [get]
func GetNodes(c *gin.Context) {
@@ -312,7 +312,7 @@ func GetNodes(c *gin.Context) {
// @Summary List apply logs
// @Tags ApplyLogs
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param node_id query string false "Node ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/apply-logs/ [get]
@@ -334,7 +334,7 @@ func GetApplyLogs(c *gin.Context) {
// @Tags ApplyLogs
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/apply-logs/cleanup [post]
func CleanupApplyLogs(c *gin.Context) {
+4 -6
View File
@@ -199,10 +199,8 @@ func OAuthCallback(c *gin.Context) {
return
}
var currentUserID *int
if value := session.Get("id"); value != nil {
if idValue, ok := value.(int); ok {
currentUserID = &idValue
}
if currentUser := currentUserFromOpenFlareToken(c); currentUser != nil {
currentUserID = &currentUser.Id
}
result, pending, err := service.CompleteOAuthLogin(source, profile, currentUserID)
if err != nil {
@@ -224,7 +222,7 @@ func OAuthCallback(c *gin.Context) {
return
}
if result.User != nil {
cleanUser, err := setLoginSession(result.User, c)
cleanUser, err := setLoginToken(result.User)
if err != nil {
respondFailure(c, "无法保存会话信息,请重试")
return
@@ -261,7 +259,7 @@ func LinkExistingOAuthAccount(c *gin.Context) {
respondFailure(c, "无法更新会话信息,请重试")
return
}
cleanUser, err := setLoginSession(user, c)
cleanUser, err := setLoginToken(user)
if err != nil {
respondFailure(c, "无法保存会话信息,请重试")
return
@@ -10,7 +10,7 @@ import (
// @Summary List config versions
// @Tags ConfigVersions
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/config-versions/ [get]
func GetConfigVersions(c *gin.Context) {
@@ -26,7 +26,7 @@ func GetConfigVersions(c *gin.Context) {
// @Summary Get config version detail
// @Tags ConfigVersions
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Version ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -48,7 +48,7 @@ func GetConfigVersion(c *gin.Context) {
// @Summary Get active config version
// @Tags ConfigVersions
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/config-versions/active [get]
func GetActiveConfigVersion(c *gin.Context) {
@@ -64,7 +64,7 @@ func GetActiveConfigVersion(c *gin.Context) {
// @Summary Preview config rendering
// @Tags ConfigVersions
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/config-versions/preview [get]
func PreviewConfigVersion(c *gin.Context) {
@@ -80,7 +80,7 @@ func PreviewConfigVersion(c *gin.Context) {
// @Summary Diff current draft against active version
// @Tags ConfigVersions
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/config-versions/diff [get]
func DiffConfigVersion(c *gin.Context) {
@@ -96,7 +96,7 @@ func DiffConfigVersion(c *gin.Context) {
// @Summary Publish a new config version
// @Tags ConfigVersions
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/config-versions/publish [post]
func PublishConfigVersion(c *gin.Context) {
@@ -114,7 +114,7 @@ func PublishConfigVersion(c *gin.Context) {
// @Summary Activate an existing config version
// @Tags ConfigVersions
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Version ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -140,7 +140,7 @@ type CleanupConfigVersionRequest struct {
// @Summary Cleanup old config versions
// @Tags ConfigVersions
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param request body CleanupConfigVersionRequest true "Cleanup request"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
+1 -1
View File
@@ -33,7 +33,7 @@ type dashboardTrendsPayload struct {
// @Summary Get dashboard overview
// @Tags Dashboard
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Router /api/dashboard/overview [get]
+1 -1
View File
@@ -11,7 +11,7 @@ import (
// @Tags Options
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/option/database/cleanup [post]
func CleanupDatabaseObservability(c *gin.Context) {
+4 -4
View File
@@ -16,7 +16,7 @@ type DnsAccountInput struct {
// @Summary List DNS accounts
// @Tags DnsAccounts
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/dns-accounts/ [get]
func GetDnsAccounts(c *gin.Context) {
@@ -33,7 +33,7 @@ func GetDnsAccounts(c *gin.Context) {
// @Tags DnsAccounts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param payload body DnsAccountInput true "DNS account payload"
// @Success 200 {object} map[string]interface{}
// @Router /api/dns-accounts/ [post]
@@ -62,7 +62,7 @@ func CreateDnsAccount(c *gin.Context) {
// @Tags DnsAccounts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "DNS Account ID"
// @Param payload body DnsAccountInput true "DNS account payload"
// @Success 200 {object} map[string]interface{}
@@ -100,7 +100,7 @@ func UpdateDnsAccount(c *gin.Context) {
// @Summary Delete DNS account
// @Tags DnsAccounts
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "DNS Account ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/dns-accounts/{id}/delete [post]
+7 -8
View File
@@ -11,7 +11,6 @@ import (
"openflare/model"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
@@ -79,9 +78,7 @@ func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
}
func GitHubOAuth(c *gin.Context) {
session := sessions.Default(c)
username := session.Get("username")
if username != nil {
if currentUserFromOpenFlareToken(c) != nil {
GitHubBind(c)
return
}
@@ -135,10 +132,12 @@ func GitHubBind(c *gin.Context) {
respondFailure(c, "该 GitHub 账户已被绑定")
return
}
session := sessions.Default(c)
id := session.Get("id")
// id := c.GetInt("id") // critical bug!
user.Id = id.(int)
currentUser := currentUserFromOpenFlareToken(c)
if currentUser == nil {
respondFailure(c, "无权进行此操作,未登录或 token 无效")
return
}
user.Id = currentUser.Id
err = user.FillUserById()
if err != nil {
respondFailure(c, err.Error())
@@ -11,7 +11,7 @@ import (
// @Summary List managed domains
// @Tags ManagedDomains
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/managed-domains/ [get]
func GetManagedDomains(c *gin.Context) {
@@ -28,7 +28,7 @@ func GetManagedDomains(c *gin.Context) {
// @Tags ManagedDomains
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param payload body service.ManagedDomainInput true "Managed domain payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -51,7 +51,7 @@ func CreateManagedDomain(c *gin.Context) {
// @Tags ManagedDomains
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Managed domain ID"
// @Param payload body service.ManagedDomainInput true "Managed domain payload"
// @Success 200 {object} map[string]interface{}
@@ -78,7 +78,7 @@ func UpdateManagedDomain(c *gin.Context) {
// @Summary Delete managed domain
// @Tags ManagedDomains
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Managed domain ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -99,7 +99,7 @@ func DeleteManagedDomain(c *gin.Context) {
// @Summary Match certificate for domain
// @Tags ManagedDomains
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param domain query string true "Domain"
// @Success 200 {object} map[string]interface{}
// @Router /api/managed-domains/match [get]
+11 -11
View File
@@ -21,7 +21,7 @@ type nodeObservabilityQuery struct {
// @Tags Nodes
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param payload body service.NodeInput true "Node payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -44,7 +44,7 @@ func CreateNode(c *gin.Context) {
// @Summary Get global discovery token
// @Tags Nodes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/nodes/bootstrap-token [get]
func GetNodeBootstrapToken(c *gin.Context) {
@@ -60,7 +60,7 @@ func GetNodeBootstrapToken(c *gin.Context) {
// @Summary Rotate global discovery token
// @Tags Nodes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/nodes/bootstrap-token/rotate [post]
func RotateNodeBootstrapToken(c *gin.Context) {
@@ -77,7 +77,7 @@ func RotateNodeBootstrapToken(c *gin.Context) {
// @Tags Nodes
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Node ID"
// @Param payload body service.NodeInput true "Node payload"
// @Success 200 {object} map[string]interface{}
@@ -106,7 +106,7 @@ func UpdateNode(c *gin.Context) {
// @Summary Delete node
// @Tags Nodes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Node ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -128,7 +128,7 @@ func DeleteNode(c *gin.Context) {
// @Summary Request agent self-update on node
// @Tags Nodes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Node ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -162,7 +162,7 @@ func RequestNodeAgentUpdate(c *gin.Context) {
// @Summary Request openresty restart on node
// @Tags Nodes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Node ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -185,7 +185,7 @@ func RequestNodeOpenrestyRestart(c *gin.Context) {
// @Summary Request force sync config on node
// @Tags Nodes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Node ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -208,7 +208,7 @@ func RequestNodeForceSync(c *gin.Context) {
// @Summary Check latest agent release for node
// @Tags Nodes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Node ID"
// @Param channel query string false "stable or preview"
// @Success 200 {object} map[string]interface{}
@@ -232,7 +232,7 @@ func GetNodeAgentRelease(c *gin.Context) {
// @Summary Get node observability details
// @Tags Nodes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Node ID"
// @Param hours query int false "Lookback window in hours"
// @Param limit query int false "Max records per section"
@@ -266,7 +266,7 @@ func GetNodeObservability(c *gin.Context) {
// @Summary Cleanup node health events
// @Tags Nodes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Node ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
+5 -5
View File
@@ -10,7 +10,7 @@ import (
// @Summary List proxy routes
// @Tags ProxyRoutes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/proxy-routes/ [get]
func GetProxyRoutes(c *gin.Context) {
@@ -26,7 +26,7 @@ func GetProxyRoutes(c *gin.Context) {
// @Summary Get proxy route detail
// @Tags ProxyRoutes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Route ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -49,7 +49,7 @@ func GetProxyRoute(c *gin.Context) {
// @Tags ProxyRoutes
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param payload body service.ProxyRouteInput true "Proxy route payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -72,7 +72,7 @@ func CreateProxyRoute(c *gin.Context) {
// @Tags ProxyRoutes
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Route ID"
// @Param payload body service.ProxyRouteInput true "Proxy route payload"
// @Success 200 {object} map[string]interface{}
@@ -99,7 +99,7 @@ func UpdateProxyRoute(c *gin.Context) {
// @Summary Delete proxy route
// @Tags ProxyRoutes
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Route ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
+11 -11
View File
@@ -10,7 +10,7 @@ import (
// @Summary List TLS certificates
// @Tags TLSCertificates
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/tls-certificates/ [get]
func GetTLSCertificates(c *gin.Context) {
@@ -26,7 +26,7 @@ func GetTLSCertificates(c *gin.Context) {
// @Summary Get TLS certificate detail
// @Tags TLSCertificates
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Certificate ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -49,7 +49,7 @@ func GetTLSCertificate(c *gin.Context) {
// @Summary Get TLS certificate PEM content
// @Tags TLSCertificates
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Certificate ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -73,7 +73,7 @@ func GetTLSCertificateContent(c *gin.Context) {
// @Tags TLSCertificates
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param payload body service.TLSCertificateInput true "TLS certificate payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -96,7 +96,7 @@ func CreateTLSCertificate(c *gin.Context) {
// @Tags TLSCertificates
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Certificate ID"
// @Param payload body service.TLSCertificateInput true "TLS certificate payload"
// @Success 200 {object} map[string]interface{}
@@ -126,7 +126,7 @@ func UpdateTLSCertificate(c *gin.Context) {
// @Tags TLSCertificates
// @Accept multipart/form-data
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param name formData string true "Certificate name"
// @Param remark formData string false "Remark"
// @Param cert_file formData file true "Certificate file"
@@ -159,7 +159,7 @@ func ImportTLSCertificateFile(c *gin.Context) {
// @Summary Delete TLS certificate
// @Tags TLSCertificates
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Certificate ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -181,7 +181,7 @@ func DeleteTLSCertificate(c *gin.Context) {
// @Tags TLSCertificates
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param payload body service.TLSApplyInput true "TLS apply payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
@@ -204,7 +204,7 @@ func ApplyTLSCertificate(c *gin.Context) {
// @Tags TLSCertificates
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Certificate ID"
// @Param payload body service.TLSApplyInput true "TLS apply payload"
// @Success 200 {object} map[string]interface{}
@@ -233,7 +233,7 @@ func UpdateAcmeCertificate(c *gin.Context) {
// @Tags TLSCertificates
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Certificate ID"
// @Param payload body service.TLSApplyInput true "TLS apply payload"
// @Success 200 {object} map[string]interface{}
@@ -261,7 +261,7 @@ func ConvertTLSCertificateToAcme(c *gin.Context) {
// @Summary Renew TLS certificate
// @Tags TLSCertificates
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Param id path int true "Certificate ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
+1 -1
View File
@@ -21,7 +21,7 @@ type serverUpgradeRequest struct {
// @Summary Get latest GitHub release
// @Tags Update
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/update/latest-release [get]
func GetLatestRelease(c *gin.Context) {
+1 -1
View File
@@ -11,7 +11,7 @@ import (
// @Tags UptimeKuma
// @Accept json
// @Produce json
// @Security BearerAuth
// @Security OpenFlareTokenAuth
// @Success 200 {object} map[string]interface{}
// @Router /api/uptimekuma/sync [post]
func SyncUptimeKuma(c *gin.Context) {
+41 -16
View File
@@ -1,6 +1,7 @@
package controller
import (
"errors"
"openflare/common"
"openflare/model"
"openflare/utils/security"
@@ -8,7 +9,6 @@ import (
"strconv"
"strings"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
@@ -45,14 +45,9 @@ func Login(c *gin.Context) {
setupLogin(&user, c)
}
// setup session & cookies and then return user info
func setLoginSession(user *model.User, c *gin.Context) (*model.User, error) {
session := sessions.Default(c)
session.Set("id", user.Id)
session.Set("username", user.Username)
session.Set("role", user.Role)
session.Set("status", user.Status)
err := session.Save()
// setup token and then return user info
func setLoginToken(user *model.User) (*model.User, error) {
token, err := ensureUserOpenFlareToken(user)
if err != nil {
return nil, err
}
@@ -62,12 +57,13 @@ func setLoginSession(user *model.User, c *gin.Context) (*model.User, error) {
DisplayName: user.DisplayName,
Role: user.Role,
Status: user.Status,
Token: token,
}
return cleanUser, nil
}
func setupLogin(user *model.User, c *gin.Context) {
cleanUser, err := setLoginSession(user, c)
cleanUser, err := setLoginToken(user)
if err != nil {
respondFailure(c, "无法保存会话信息,请重试")
return
@@ -76,16 +72,45 @@ func setupLogin(user *model.User, c *gin.Context) {
}
func Logout(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
err := session.Save()
if err != nil {
respondFailure(c, err.Error())
return
token := c.GetHeader("OPENFLARE_TOKEN")
if token != "" {
user := model.ValidateUserToken(token)
if user != nil && user.Id != 0 {
if err := model.DB.Model(user).Update("token", "").Error; err != nil {
respondFailure(c, err.Error())
return
}
}
}
respondSuccessMessage(c, "")
}
func ensureUserOpenFlareToken(user *model.User) (string, error) {
if user.Token != "" {
return user.Token, nil
}
for i := 0; i < 3; i++ {
token := strings.Replace(uuid.New().String(), "-", "", -1)
if model.DB.Where("token = ?", token).First(&model.User{}).RowsAffected != 0 {
continue
}
if err := model.DB.Model(user).Update("token", token).Error; err != nil {
return "", err
}
user.Token = token
return token, nil
}
return "", errors.New("生成登录凭证失败,请重试")
}
func currentUserFromOpenFlareToken(c *gin.Context) *model.User {
token := c.GetHeader("OPENFLARE_TOKEN")
if token == "" {
return nil
}
return model.ValidateUserToken(token)
}
func Register(c *gin.Context) {
respondFailure(c, "非法请求")
}
+60 -60
View File
@@ -19,7 +19,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -94,7 +94,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -122,7 +122,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -203,7 +203,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -292,7 +292,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -361,7 +361,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -419,7 +419,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -667,7 +667,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -700,7 +700,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -728,7 +728,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -753,7 +753,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -778,7 +778,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -821,7 +821,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -846,7 +846,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -871,7 +871,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -896,7 +896,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -937,7 +937,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -978,7 +978,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1010,7 +1010,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1033,7 +1033,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1072,7 +1072,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1106,7 +1106,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1276,7 +1276,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1299,7 +1299,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1345,7 +1345,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1379,7 +1379,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1420,7 +1420,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1473,7 +1473,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1496,7 +1496,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1542,7 +1542,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1567,7 +1567,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1592,7 +1592,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1639,7 +1639,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1680,7 +1680,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1721,7 +1721,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1762,7 +1762,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1815,7 +1815,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1856,7 +1856,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1897,7 +1897,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1970,7 +1970,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2121,7 +2121,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2144,7 +2144,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2190,7 +2190,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2231,7 +2231,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2272,7 +2272,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2405,7 +2405,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2428,7 +2428,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2474,7 +2474,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2520,7 +2520,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2584,7 +2584,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2625,7 +2625,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2666,7 +2666,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2719,7 +2719,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2760,7 +2760,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2801,7 +2801,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2854,7 +2854,7 @@ const docTemplate = `{
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2907,7 +2907,7 @@ const docTemplate = `{
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -3761,10 +3761,10 @@ const docTemplate = `{
"name": "X-Agent-Token",
"in": "header"
},
"BearerAuth": {
"description": "管理端可使用 Bearer Token,例如:Bearer \u003ctoken\u003e",
"OpenFlareTokenAuth": {
"description": "管理端 API 使用登录后返回的用户 Token",
"type": "apiKey",
"name": "Authorization",
"name": "OPENFLARE_TOKEN",
"in": "header"
}
}
+60 -60
View File
@@ -16,7 +16,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -91,7 +91,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -119,7 +119,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -200,7 +200,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -289,7 +289,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -358,7 +358,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -416,7 +416,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -664,7 +664,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -697,7 +697,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -725,7 +725,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -750,7 +750,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -775,7 +775,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -818,7 +818,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -843,7 +843,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -868,7 +868,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -893,7 +893,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -934,7 +934,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -975,7 +975,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1007,7 +1007,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1030,7 +1030,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1069,7 +1069,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1103,7 +1103,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1273,7 +1273,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1296,7 +1296,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1342,7 +1342,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1376,7 +1376,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1417,7 +1417,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1470,7 +1470,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1493,7 +1493,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1539,7 +1539,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1564,7 +1564,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1589,7 +1589,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1636,7 +1636,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1677,7 +1677,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1718,7 +1718,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1759,7 +1759,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1812,7 +1812,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1853,7 +1853,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -1894,7 +1894,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -1967,7 +1967,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2118,7 +2118,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2141,7 +2141,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2187,7 +2187,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2228,7 +2228,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2269,7 +2269,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2402,7 +2402,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2425,7 +2425,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2471,7 +2471,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2517,7 +2517,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2581,7 +2581,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2622,7 +2622,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2663,7 +2663,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2716,7 +2716,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2757,7 +2757,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -2798,7 +2798,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2851,7 +2851,7 @@
"post": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"consumes": [
@@ -2904,7 +2904,7 @@
"get": {
"security": [
{
"BearerAuth": []
"OpenFlareTokenAuth": []
}
],
"produces": [
@@ -3758,10 +3758,10 @@
"name": "X-Agent-Token",
"in": "header"
},
"BearerAuth": {
"description": "管理端可使用 Bearer Token,例如:Bearer \u003ctoken\u003e",
"OpenFlareTokenAuth": {
"description": "管理端 API 使用登录后返回的用户 Token",
"type": "apiKey",
"name": "Authorization",
"name": "OPENFLARE_TOKEN",
"in": "header"
}
}
+60 -60
View File
@@ -541,7 +541,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List access logs
tags:
- AccessLogs
@@ -558,7 +558,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Cleanup access logs by retention days
tags:
- AccessLogs
@@ -610,7 +610,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List folded access logs
tags:
- AccessLogs
@@ -668,7 +668,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List folded access log IP summaries
tags:
- AccessLogs
@@ -712,7 +712,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List access log IP summaries
tags:
- AccessLogs
@@ -749,7 +749,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get access log IP trend
tags:
- AccessLogs
@@ -764,7 +764,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get default ACME account
tags:
- AcmeAccounts
@@ -923,7 +923,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List apply logs
tags:
- ApplyLogs
@@ -940,7 +940,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Cleanup apply logs
tags:
- ApplyLogs
@@ -955,7 +955,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List config versions
tags:
- ConfigVersions
@@ -981,7 +981,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get config version detail
tags:
- ConfigVersions
@@ -1007,7 +1007,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Activate an existing config version
tags:
- ConfigVersions
@@ -1022,7 +1022,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get active config version
tags:
- ConfigVersions
@@ -1049,7 +1049,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Cleanup old config versions
tags:
- ConfigVersions
@@ -1064,7 +1064,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Diff current draft against active version
tags:
- ConfigVersions
@@ -1079,7 +1079,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Preview config rendering
tags:
- ConfigVersions
@@ -1094,7 +1094,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Publish a new config version
tags:
- ConfigVersions
@@ -1114,7 +1114,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get dashboard overview
tags:
- Dashboard
@@ -1129,7 +1129,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List DNS accounts
tags:
- DnsAccounts
@@ -1152,7 +1152,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Create DNS account
tags:
- DnsAccounts
@@ -1173,7 +1173,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Delete DNS account
tags:
- DnsAccounts
@@ -1202,7 +1202,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Update DNS account
tags:
- DnsAccounts
@@ -1293,7 +1293,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List managed domains
tags:
- ManagedDomains
@@ -1321,7 +1321,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Create managed domain
tags:
- ManagedDomains
@@ -1347,7 +1347,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Delete managed domain
tags:
- ManagedDomains
@@ -1381,7 +1381,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Update managed domain
tags:
- ManagedDomains
@@ -1402,7 +1402,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Match certificate for domain
tags:
- ManagedDomains
@@ -1417,7 +1417,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List nodes
tags:
- Nodes
@@ -1445,7 +1445,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Create node
tags:
- Nodes
@@ -1475,7 +1475,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Check latest agent release for node
tags:
- Nodes
@@ -1501,7 +1501,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Request agent self-update on node
tags:
- Nodes
@@ -1527,7 +1527,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Delete node
tags:
- Nodes
@@ -1553,7 +1553,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Request force sync config on node
tags:
- Nodes
@@ -1587,7 +1587,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get node observability details
tags:
- Nodes
@@ -1613,7 +1613,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Cleanup node health events
tags:
- Nodes
@@ -1639,7 +1639,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Request openresty restart on node
tags:
- Nodes
@@ -1673,7 +1673,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Update node
tags:
- Nodes
@@ -1688,7 +1688,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get global discovery token
tags:
- Nodes
@@ -1703,7 +1703,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Rotate global discovery token
tags:
- Nodes
@@ -1733,7 +1733,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Cleanup observability tables
tags:
- Options
@@ -1829,7 +1829,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List proxy routes
tags:
- ProxyRoutes
@@ -1857,7 +1857,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Create proxy route
tags:
- ProxyRoutes
@@ -1883,7 +1883,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get proxy route detail
tags:
- ProxyRoutes
@@ -1909,7 +1909,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Delete proxy route
tags:
- ProxyRoutes
@@ -1943,7 +1943,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Update proxy route
tags:
- ProxyRoutes
@@ -2008,7 +2008,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: List TLS certificates
tags:
- TLSCertificates
@@ -2036,7 +2036,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Create TLS certificate from PEM
tags:
- TLSCertificates
@@ -2062,7 +2062,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get TLS certificate detail
tags:
- TLSCertificates
@@ -2088,7 +2088,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get TLS certificate PEM content
tags:
- TLSCertificates
@@ -2122,7 +2122,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Convert uploaded TLS certificate to ACME managed certificate
tags:
- TLSCertificates
@@ -2148,7 +2148,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Delete TLS certificate
tags:
- TLSCertificates
@@ -2174,7 +2174,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Renew TLS certificate
tags:
- TLSCertificates
@@ -2208,7 +2208,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Update TLS certificate from PEM
tags:
- TLSCertificates
@@ -2242,7 +2242,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Update ACME TLS certificate
tags:
- TLSCertificates
@@ -2271,7 +2271,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Apply TLS certificate via ACME
tags:
- TLSCertificates
@@ -2313,7 +2313,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Import TLS certificate from files
tags:
- TLSCertificates
@@ -2328,7 +2328,7 @@ paths:
additionalProperties: true
type: object
security:
- BearerAuth: []
- OpenFlareTokenAuth: []
summary: Get latest GitHub release
tags:
- Update
@@ -2390,9 +2390,9 @@ securityDefinitions:
in: header
name: X-Agent-Token
type: apiKey
BearerAuth:
description: 管理端可使用 Bearer Token,例如:Bearer <token>
OpenFlareTokenAuth:
description: 管理端 API 使用登录后返回的用户 Token
in: header
name: Authorization
name: OPENFLARE_TOKEN
type: apiKey
swagger: "2.0"
+3 -3
View File
@@ -33,10 +33,10 @@ var indexPage []byte
// @description OpenFlare Server 管理端与 Agent API 文档。
// @BasePath /
// @schemes http https
// @securityDefinitions.apikey BearerAuth
// @securityDefinitions.apikey OpenFlareTokenAuth
// @in header
// @name Authorization
// @description 管理端可使用 Bearer Token,例如:Bearer <token>
// @name OPENFLARE_TOKEN
// @description 管理端 API 使用登录后返回的用户 Token
// @securityDefinitions.apikey AccessTokenAuth
// @in header
// @name X-Agent-Token
+28 -60
View File
@@ -1,49 +1,35 @@
package middleware
import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"net/http"
"openflare/common"
"openflare/model"
)
const OpenFlareTokenHeader = "OPENFLARE_TOKEN"
func authHelper(c *gin.Context, minRole int) {
session := sessions.Default(c)
username := session.Get("username")
role := session.Get("role")
id := session.Get("id")
status := session.Get("status")
authByToken := false
if username == nil {
// Check token
token := c.Request.Header.Get("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "无权进行此操作,未登录或 token 无效",
})
c.Abort()
return
}
user := model.ValidateUserToken(token)
if user != nil && user.Username != "" {
// Token is valid
username = user.Username
role = user.Role
id = user.Id
status = user.Status
} else {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无权进行此操作,token 无效",
})
c.Abort()
return
}
authByToken = true
token := c.GetHeader(OpenFlareTokenHeader)
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "无权进行此操作,未登录或 token 无效",
})
c.Abort()
return
}
if status.(int) == common.UserStatusDisabled {
user := model.ValidateUserToken(token)
if user == nil || user.Username == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "无权进行此操作,token 无效",
})
c.Abort()
return
}
if user.Status == common.UserStatusDisabled {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "用户已被封禁",
@@ -51,7 +37,7 @@ func authHelper(c *gin.Context, minRole int) {
c.Abort()
return
}
if role.(int) < minRole {
if user.Role < minRole {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无权进行此操作,权限不足",
@@ -59,10 +45,10 @@ func authHelper(c *gin.Context, minRole int) {
c.Abort()
return
}
c.Set("username", username)
c.Set("role", role)
c.Set("id", id)
c.Set("authByToken", authByToken)
c.Set("username", user.Username)
c.Set("role", user.Role)
c.Set("id", user.Id)
c.Set("authByToken", true)
c.Next()
}
@@ -84,34 +70,16 @@ func RootAuth() func(c *gin.Context) {
}
}
// NoTokenAuth You should always use this after normal auth middlewares.
// NoTokenAuth is kept as a compatibility no-op because admin APIs now always use OPENFLARE_TOKEN.
func NoTokenAuth() func(c *gin.Context) {
return func(c *gin.Context) {
authByToken := c.GetBool("authByToken")
if authByToken {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "本接口不支持使用 token 进行验证",
})
c.Abort()
return
}
c.Next()
}
}
// TokenOnlyAuth You should always use this after normal auth middlewares.
// TokenOnlyAuth is kept as a compatibility no-op because admin APIs now always use OPENFLARE_TOKEN.
func TokenOnlyAuth() func(c *gin.Context) {
return func(c *gin.Context) {
authByToken := c.GetBool("authByToken")
if !authByToken {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "本接口仅支持使用 token 进行验证",
})
c.Abort()
return
}
c.Next()
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
func CORS() gin.HandlerFunc {
config := cors.DefaultConfig()
config.AllowCredentials = true
config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization", "X-Agent-Token", "Accept"}
config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization", "OPENFLARE_TOKEN", "X-Agent-Token", "Accept"}
config.AllowOriginFunc = func(origin string) bool {
serverAddr := strings.TrimRight(common.ServerAddress, "/")
if serverAddr == "" {
+4 -4
View File
@@ -95,7 +95,7 @@ func TestPhase1PublishLifecycle(t *testing.T) {
}
repeatPublishReq := httptest.NewRequest(http.MethodPost, "/api/config-versions/publish", nil)
repeatPublishReq.Header.Set("Authorization", "Bearer "+token)
repeatPublishReq.Header.Set("OPENFLARE_TOKEN", token)
repeatPublishRecorder := httptest.NewRecorder()
engine.ServeHTTP(repeatPublishRecorder, repeatPublishReq)
if repeatPublishRecorder.Code != http.StatusOK {
@@ -519,7 +519,7 @@ func performJSONRequest(t *testing.T, engine http.Handler, token string, method
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("OPENFLARE_TOKEN", token)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, req)
if recorder.Code != http.StatusOK {
@@ -549,7 +549,7 @@ func performJSONRequestNoFatal(t *testing.T, engine http.Handler, token string,
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("OPENFLARE_TOKEN", token)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, req)
if recorder.Code != http.StatusOK && recorder.Code != http.StatusBadRequest {
@@ -596,7 +596,7 @@ func performMultipartRequest(t *testing.T, engine http.Handler, token string, pa
}
req := httptest.NewRequest(http.MethodPost, path, &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("OPENFLARE_TOKEN", token)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, req)
if recorder.Code != http.StatusOK {
+11 -10
View File
@@ -127,7 +127,7 @@ func TestPhase2BatchOptionUpdateIsAtomic(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/option/update-batch", bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
req.AddCookie(loginCookie)
req.Header.Set("OPENFLARE_TOKEN", loginCookie)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, req)
@@ -320,7 +320,7 @@ func TestExternalAccountBindingsCanBeListedAndDeleted(t *testing.T) {
}
}
func loginAsRoot(t *testing.T, engine http.Handler) *http.Cookie {
func loginAsRoot(t *testing.T, engine http.Handler) string {
t.Helper()
payload, err := json.Marshal(map[string]any{
"username": "root",
@@ -346,16 +346,17 @@ func loginAsRoot(t *testing.T, engine http.Handler) *http.Cookie {
t.Fatalf("root login failed: %s", resp.Message)
}
for _, cookie := range recorder.Result().Cookies() {
if cookie.Name == "session" {
return cookie
}
var user model.User
if err = json.Unmarshal(resp.Data, &user); err != nil {
t.Fatalf("failed to decode login user: %v", err)
}
t.Fatal("expected session cookie after root login")
return nil
if user.Token == "" {
t.Fatal("expected OPENFLARE_TOKEN after root login")
}
return user.Token
}
func performSessionJSONRequest(t *testing.T, engine http.Handler, sessionCookie *http.Cookie, method string, path string, body any) apiResponse {
func performSessionJSONRequest(t *testing.T, engine http.Handler, token string, method string, path string, body any) apiResponse {
t.Helper()
var payload []byte
var err error
@@ -370,7 +371,7 @@ func performSessionJSONRequest(t *testing.T, engine http.Handler, sessionCookie
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.AddCookie(sessionCookie)
req.Header.Set("OPENFLARE_TOKEN", token)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, req)
@@ -133,7 +133,7 @@ func TestUptimeKumaSyncDisabled(t *testing.T) {
// Request sync, should fail
req := httptest.NewRequest(http.MethodPost, "/api/uptimekuma/sync", nil)
req.AddCookie(loginCookie)
req.Header.Set("OPENFLARE_TOKEN", loginCookie)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, req)
@@ -265,7 +265,7 @@ func TestUptimeKumaSyncSuccess(t *testing.T) {
loginCookie := loginAsRoot(t, engine)
req := httptest.NewRequest(http.MethodPost, "/api/uptimekuma/sync", nil)
req.AddCookie(loginCookie)
req.Header.Set("OPENFLARE_TOKEN", loginCookie)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, req)
@@ -405,7 +405,7 @@ func TestUptimeKumaSyncSelectedScope(t *testing.T) {
loginCookie := loginAsRoot(t, engine)
req := httptest.NewRequest(http.MethodPost, "/api/uptimekuma/sync", nil)
req.AddCookie(loginCookie)
req.Header.Set("OPENFLARE_TOKEN", loginCookie)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, req)
+34 -20
View File
@@ -77,13 +77,22 @@ func TestLatestReleaseProxy(t *testing.T) {
if loginRecorder.Code != http.StatusOK {
t.Fatalf("unexpected login status code: %d", loginRecorder.Code)
}
loginResult := loginRecorder.Result()
defer loginResult.Body.Close()
var loginResp apiResponse
if err = json.Unmarshal(loginRecorder.Body.Bytes(), &loginResp); err != nil {
t.Fatalf("failed to decode login response: %v", err)
}
var loginUser struct {
Token string `json:"token"`
}
if err = json.Unmarshal(loginResp.Data, &loginUser); err != nil {
t.Fatalf("failed to decode login user: %v", err)
}
if loginUser.Token == "" {
t.Fatal("expected OPENFLARE_TOKEN after login")
}
req := httptest.NewRequest(http.MethodGet, "/api/update/latest-release", nil)
for _, cookieValue := range loginResult.Cookies() {
req.AddCookie(cookieValue)
}
req.Header.Set("OPENFLARE_TOKEN", loginUser.Token)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, req)
@@ -111,7 +120,7 @@ func TestLatestReleaseProxy(t *testing.T) {
}
}
func loginRootAndBuildEngine(t *testing.T) (*gin.Engine, []*http.Cookie) {
func loginRootAndBuildEngine(t *testing.T) (*gin.Engine, string) {
t.Helper()
gin.SetMode(gin.TestMode)
common.RedisEnabled = false
@@ -135,10 +144,21 @@ func loginRootAndBuildEngine(t *testing.T) (*gin.Engine, []*http.Cookie) {
if loginRecorder.Code != http.StatusOK {
t.Fatalf("unexpected login status code: %d", loginRecorder.Code)
}
loginResult := loginRecorder.Result()
defer loginResult.Body.Close()
var loginResp apiResponse
if err = json.Unmarshal(loginRecorder.Body.Bytes(), &loginResp); err != nil {
t.Fatalf("failed to decode login response: %v", err)
}
var loginUser struct {
Token string `json:"token"`
}
if err = json.Unmarshal(loginResp.Data, &loginUser); err != nil {
t.Fatalf("failed to decode login user: %v", err)
}
if loginUser.Token == "" {
t.Fatal("expected OPENFLARE_TOKEN after login")
}
return engine, loginResult.Cookies()
return engine, loginUser.Token
}
func fakeManualServerBinary(version string) (string, []byte) {
@@ -157,7 +177,7 @@ func TestManualUploadRoute(t *testing.T) {
service.SetServerUpgradeDispatchDelayForTest(500 * time.Millisecond)
})
engine, cookies := loginRootAndBuildEngine(t)
engine, token := loginRootAndBuildEngine(t)
fileName, content := fakeManualServerBinary("v0.5.0")
body := &bytes.Buffer{}
@@ -175,9 +195,7 @@ func TestManualUploadRoute(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/update/manual-upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
for _, cookieValue := range cookies {
req.AddCookie(cookieValue)
}
req.Header.Set("OPENFLARE_TOKEN", token)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, req)
@@ -225,7 +243,7 @@ func TestManualUpgradeConfirmRoute(t *testing.T) {
service.SetServerUpgradeDispatchDelayForTest(originalDelay)
})
engine, cookies := loginRootAndBuildEngine(t)
engine, token := loginRootAndBuildEngine(t)
fileName, content := fakeManualServerBinary("v0.5.0")
body := &bytes.Buffer{}
@@ -243,9 +261,7 @@ func TestManualUpgradeConfirmRoute(t *testing.T) {
uploadReq := httptest.NewRequest(http.MethodPost, "/api/update/manual-upload", body)
uploadReq.Header.Set("Content-Type", writer.FormDataContentType())
for _, cookieValue := range cookies {
uploadReq.AddCookie(cookieValue)
}
uploadReq.Header.Set("OPENFLARE_TOKEN", token)
uploadRecorder := httptest.NewRecorder()
engine.ServeHTTP(uploadRecorder, uploadReq)
@@ -276,9 +292,7 @@ func TestManualUpgradeConfirmRoute(t *testing.T) {
}
confirmReq := httptest.NewRequest(http.MethodPost, "/api/update/manual-upgrade", bytes.NewReader(confirmBody))
confirmReq.Header.Set("Content-Type", "application/json")
for _, cookieValue := range cookies {
confirmReq.AddCookie(cookieValue)
}
confirmReq.Header.Set("OPENFLARE_TOKEN", token)
confirmRecorder := httptest.NewRecorder()
engine.ServeHTTP(confirmRecorder, confirmReq)
@@ -14,6 +14,7 @@ import {
logout as logoutRequest,
getCurrentUser,
} from '@/features/auth/api/auth';
import { clearStoredOpenFlareToken } from '@/lib/api/auth-token';
import type { AuthUser } from '@/types/auth';
interface AuthContextValue {
@@ -41,6 +42,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
setUserState(nextUser);
return nextUser;
} catch {
clearStoredOpenFlareToken();
setUserState(null);
return null;
} finally {
+31 -3
View File
@@ -1,4 +1,8 @@
import { apiRequest } from '@/lib/api/client';
import {
clearStoredOpenFlareToken,
setStoredOpenFlareToken,
} from '@/lib/api/auth-token';
import type {
AuthUser,
LoginPayload,
@@ -14,11 +18,18 @@ export function login(payload: LoginPayload) {
return apiRequest<AuthUser>('/user/login', {
method: 'POST',
body: JSON.stringify(payload),
}).then((user) => {
if (user.token) {
setStoredOpenFlareToken(user.token);
}
return user;
});
}
export function logout() {
return apiRequest<void>('/user/logout');
return apiRequest<void>('/user/logout').finally(() => {
clearStoredOpenFlareToken();
});
}
export function register(payload: RegisterPayload) {
@@ -48,7 +59,14 @@ export function resetPassword(payload: PasswordResetRequestPayload) {
}
export function exchangeGitHubCode(code: string) {
return apiRequest<AuthUser>(`/oauth/github?code=${encodeURIComponent(code)}`);
return apiRequest<AuthUser>(
`/oauth/github?code=${encodeURIComponent(code)}`,
).then((user) => {
if (user.token) {
setStoredOpenFlareToken(user.token);
}
return user;
});
}
export interface OAuthAuthorizeResult {
@@ -79,12 +97,22 @@ export function exchangeOAuthCode(
const searchParams = new URLSearchParams({ code, state });
return apiRequest<OAuthCallbackResult>(
`/oauth/${encodeURIComponent(String(source))}/callback?${searchParams.toString()}`,
);
).then((result) => {
if (result.user?.token) {
setStoredOpenFlareToken(result.user.token);
}
return result;
});
}
export function linkExistingOAuthAccount(payload: LinkExistingOAuthPayload) {
return apiRequest<OAuthCallbackResult>('/oauth/link-existing', {
method: 'POST',
body: JSON.stringify(payload),
}).then((result) => {
if (result.user?.token) {
setStoredOpenFlareToken(result.user.token);
}
return result;
});
}
@@ -1,4 +1,5 @@
import { apiRequest } from '@/lib/api/client';
import { setStoredOpenFlareToken } from '@/lib/api/auth-token';
import type {
BootstrapTokenPayload,
@@ -109,7 +110,10 @@ export function updateSelf(payload: UpdateSelfPayload) {
}
export function generateAccessToken() {
return apiRequest<string>('/user/token');
return apiRequest<string>('/user/token').then((token) => {
setStoredOpenFlareToken(token);
return token;
});
}
export function bindWeChat(code: string) {
@@ -0,0 +1,22 @@
const openFlareTokenStorageKey = 'openflare_token';
export function getStoredOpenFlareToken() {
if (typeof window === 'undefined') {
return '';
}
return window.localStorage.getItem(openFlareTokenStorageKey) || '';
}
export function setStoredOpenFlareToken(token: string) {
if (typeof window === 'undefined') {
return;
}
window.localStorage.setItem(openFlareTokenStorageKey, token);
}
export function clearStoredOpenFlareToken() {
if (typeof window === 'undefined') {
return;
}
window.localStorage.removeItem(openFlareTokenStorageKey);
}
+7
View File
@@ -1,4 +1,5 @@
import { publicEnv } from '@/lib/env/public-env';
import { getStoredOpenFlareToken } from '@/lib/api/auth-token';
import type { ApiEnvelope } from '@/types/api';
export class ApiError extends Error {
@@ -23,6 +24,12 @@ export async function apiRequest<T>(path: string, init?: RequestInit) {
if (!(init?.body instanceof FormData) && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
if (!headers.has('OPENFLARE_TOKEN')) {
const token = getStoredOpenFlareToken();
if (token) {
headers.set('OPENFLARE_TOKEN', token);
}
}
const response = await fetch(getApiUrl(path), {
credentials: 'include',
+1
View File
@@ -4,6 +4,7 @@ export interface AuthUser {
display_name: string;
role: number;
status: number;
token?: string;
email?: string;
github_id?: string;
wechat_id?: string;