diff --git a/config/config.yaml b/config/config.yaml index 506958a..4ebbf4b 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,40 +1,35 @@ site: - name: "文件暂存柜" - description: "临时文件中转服务" - + name: 文件暂存柜 + description: 临时文件中转服务 security: - # 管理员密码的 bcrypt 哈希。如果首次运行且此处为空,系统将自动生成随机密码并打印在控制台 - admin_password_hash: "$2a$10$Bm0TEmU4uj.bVHYiIPFBheUkcdg6XHpsanLvmpoAtgU1UnKbo9.vy" # 默认密码: admin - pickup_code_length: 6 - pickup_fail_limit: 5 - jwt_secret: "file-relay-secret" - + admin_password_hash: $2a$10$Bm0TEmU4uj.bVHYiIPFBheUkcdg6XHpsanLvmpoAtgU1UnKbo9.vy + pickup_code_length: 6 + pickup_fail_limit: 5 + jwt_secret: file-relay-secret upload: - max_file_size_mb: 500 - max_batch_files: 20 - max_retention_days: 30 - + max_file_size_mb: 100 + max_batch_files: 20 + max_retention_days: 30 + require_token: false storage: - type: "local" - local: - path: "storage_data" - webdav: - url: "https://dav.example.com" - username: "user" - password: "pass" - root: "/file-relay" - s3: - endpoint: "s3.amazonaws.com" - region: "us-east-1" - access_key: "your-access-key" - secret_key: "your-secret-key" - bucket: "file-relay-bucket" - use_ssl: true - + type: local + local: + path: storage_data_test + webdav: + url: https://dav.example.com + username: user + password: pass + root: /file-relay + s3: + endpoint: s3.amazonaws.com + region: us-east-1 + access_key: your-access-key + secret_key: your-secret-key + bucket: file-relay-bucket + use_ssl: false api_token: - enabled: true - allow_admin_api: false - max_tokens: 20 - + enabled: true + allow_admin_api: true + max_tokens: 20 database: - path: "file_relay.db" + path: file_relay.db diff --git a/docs/docs.go b/docs/docs.go index a5f9500..7b778ca 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -31,7 +31,7 @@ const docTemplate = `{ "AdminAuth": [] } ], - "description": "获取系统中所有 API Token 的详详信息(不包含哈希)", + "description": "获取系统中所有 API Token 的详细信息(不包含哈希)", "produces": [ "application/json" ], @@ -445,7 +445,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/config.Config" + "allOf": [ + { + "$ref": "#/definitions/model.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config.Config" + } + } + } + ] } } } @@ -482,7 +494,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/model.Response" + "allOf": [ + { + "$ref": "#/definitions/model.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config.Config" + } + } + } + ] } }, "400": { @@ -554,7 +578,12 @@ const docTemplate = `{ }, "/api/batches": { "post": { - "description": "上传一个或多个文件并创建一个提取批次", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "上传一个或多个文件并创建一个提取批次。如果配置了 require_token,则必须提供带 upload scope 的 API Token。", "consumes": [ "multipart/form-data" ], @@ -634,7 +663,12 @@ const docTemplate = `{ }, "/api/batches/text": { "post": { - "description": "中转一段长文本内容并创建一个提取批次", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "中转一段长文本内容并创建一个提取批次。如果配置了 require_token,则必须提供带 upload scope 的 API Token。", "consumes": [ "application/json" ], @@ -692,7 +726,12 @@ const docTemplate = `{ }, "/api/batches/{pickup_code}": { "get": { - "description": "根据取件码获取文件批次详详情和文件列表", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "根据取件码获取文件批次详细信息和文件列表。可选提供带 pickup scope 的 API Token。", "produces": [ "application/json" ], @@ -739,7 +778,12 @@ const docTemplate = `{ }, "/api/batches/{pickup_code}/download": { "get": { - "description": "根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载。可选提供带 pickup scope 的 API Token。", "produces": [ "application/zip" ], @@ -806,7 +850,12 @@ const docTemplate = `{ }, "/api/files/{file_id}/download": { "get": { - "description": "根据文件 ID 下载单个文件", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。", "produces": [ "application/octet-stream" ], @@ -940,13 +989,16 @@ const docTemplate = `{ "config.APITokenConfig": { "type": "object", "properties": { - "allowAdminAPI": { + "allow_admin_api": { + "description": "是否允许 API Token 访问管理接口", "type": "boolean" }, "enabled": { + "description": "是否启用 API Token", "type": "boolean" }, - "maxTokens": { + "max_tokens": { + "description": "最大 Token 数量", "type": "integer" } } @@ -954,23 +1006,53 @@ const docTemplate = `{ "config.Config": { "type": "object", "properties": { - "apitoken": { - "$ref": "#/definitions/config.APITokenConfig" + "api_token": { + "description": "API Token 设置", + "allOf": [ + { + "$ref": "#/definitions/config.APITokenConfig" + } + ] }, "database": { - "$ref": "#/definitions/config.DatabaseConfig" + "description": "数据库设置", + "allOf": [ + { + "$ref": "#/definitions/config.DatabaseConfig" + } + ] }, "security": { - "$ref": "#/definitions/config.SecurityConfig" + "description": "安全设置", + "allOf": [ + { + "$ref": "#/definitions/config.SecurityConfig" + } + ] }, "site": { - "$ref": "#/definitions/config.SiteConfig" + "description": "站点设置", + "allOf": [ + { + "$ref": "#/definitions/config.SiteConfig" + } + ] }, "storage": { - "$ref": "#/definitions/config.StorageConfig" + "description": "存储设置", + "allOf": [ + { + "$ref": "#/definitions/config.StorageConfig" + } + ] }, "upload": { - "$ref": "#/definitions/config.UploadConfig" + "description": "上传设置", + "allOf": [ + { + "$ref": "#/definitions/config.UploadConfig" + } + ] } } }, @@ -978,6 +1060,7 @@ const docTemplate = `{ "type": "object", "properties": { "path": { + "description": "数据库文件路径", "type": "string" } } @@ -985,16 +1068,20 @@ const docTemplate = `{ "config.SecurityConfig": { "type": "object", "properties": { - "adminPasswordHash": { + "admin_password_hash": { + "description": "管理员密码哈希 (bcrypt)", "type": "string" }, - "jwtsecret": { + "jwt_secret": { + "description": "JWT 签名密钥", "type": "string" }, - "pickupCodeLength": { + "pickup_code_length": { + "description": "取件码长度", "type": "integer" }, - "pickupFailLimit": { + "pickup_fail_limit": { + "description": "取件失败尝试限制", "type": "integer" } } @@ -1003,9 +1090,11 @@ const docTemplate = `{ "type": "object", "properties": { "description": { + "description": "站点描述", "type": "string" }, "name": { + "description": "站点名称", "type": "string" } } @@ -1017,6 +1106,7 @@ const docTemplate = `{ "type": "object", "properties": { "path": { + "description": "本地存储路径", "type": "string" } } @@ -1024,42 +1114,53 @@ const docTemplate = `{ "s3": { "type": "object", "properties": { - "accessKey": { + "access_key": { + "description": "S3 Access Key", "type": "string" }, "bucket": { + "description": "S3 Bucket", "type": "string" }, "endpoint": { + "description": "S3 端点", "type": "string" }, "region": { + "description": "S3 区域", "type": "string" }, - "secretKey": { + "secret_key": { + "description": "S3 Secret Key", "type": "string" }, - "useSSL": { + "use_ssl": { + "description": "是否使用 SSL", "type": "boolean" } } }, "type": { + "description": "存储类型: local, webdav, s3", "type": "string" }, - "webDAV": { + "webdav": { "type": "object", "properties": { "password": { + "description": "WebDAV 密码", "type": "string" }, "root": { + "description": "WebDAV 根目录", "type": "string" }, "url": { + "description": "WebDAV 地址", "type": "string" }, "username": { + "description": "WebDAV 用户名", "type": "string" } } @@ -1069,14 +1170,21 @@ const docTemplate = `{ "config.UploadConfig": { "type": "object", "properties": { - "maxBatchFiles": { + "max_batch_files": { + "description": "每个批次最大文件数", "type": "integer" }, - "maxFileSizeMB": { + "max_file_size_mb": { + "description": "单个文件最大大小 (MB)", "type": "integer" }, - "maxRetentionDays": { + "max_retention_days": { + "description": "最大保留天数", "type": "integer" + }, + "require_token": { + "description": "是否强制要求上传 Token", + "type": "boolean" } } }, @@ -1228,25 +1336,50 @@ const docTemplate = `{ } } }, + "public.PublicAPITokenConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "public.PublicConfig": { "type": "object", "properties": { "api_token": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - } - } + "$ref": "#/definitions/public.PublicAPITokenConfig" + }, + "security": { + "$ref": "#/definitions/public.PublicSecurityConfig" }, "site": { "$ref": "#/definitions/config.SiteConfig" }, + "storage": { + "$ref": "#/definitions/public.PublicStorageConfig" + }, "upload": { "$ref": "#/definitions/config.UploadConfig" } } }, + "public.PublicSecurityConfig": { + "type": "object", + "properties": { + "pickup_code_length": { + "type": "integer" + } + } + }, + "public.PublicStorageConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, "public.UploadResponse": { "type": "object", "properties": { @@ -1291,8 +1424,14 @@ const docTemplate = `{ } }, "securityDefinitions": { + "APITokenAuth": { + "description": "Type \"Bearer \u003cAPI-Token\u003e\" to authenticate. Required scope depends on the endpoint.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, "AdminAuth": { - "description": "Type \"Bearer \u003cyour-jwt-token\u003e\" to authenticate.", + "description": "Type \"Bearer \u003cJWT-Token\u003e\" or \"Bearer \u003cAPI-Token\u003e\" to authenticate. API Token must have 'admin' scope.", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/docs/swagger.json b/docs/swagger.json index f806bf9..2f87ab4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -24,7 +24,7 @@ "AdminAuth": [] } ], - "description": "获取系统中所有 API Token 的详详信息(不包含哈希)", + "description": "获取系统中所有 API Token 的详细信息(不包含哈希)", "produces": [ "application/json" ], @@ -438,7 +438,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/config.Config" + "allOf": [ + { + "$ref": "#/definitions/model.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config.Config" + } + } + } + ] } } } @@ -475,7 +487,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/model.Response" + "allOf": [ + { + "$ref": "#/definitions/model.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config.Config" + } + } + } + ] } }, "400": { @@ -547,7 +571,12 @@ }, "/api/batches": { "post": { - "description": "上传一个或多个文件并创建一个提取批次", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "上传一个或多个文件并创建一个提取批次。如果配置了 require_token,则必须提供带 upload scope 的 API Token。", "consumes": [ "multipart/form-data" ], @@ -627,7 +656,12 @@ }, "/api/batches/text": { "post": { - "description": "中转一段长文本内容并创建一个提取批次", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "中转一段长文本内容并创建一个提取批次。如果配置了 require_token,则必须提供带 upload scope 的 API Token。", "consumes": [ "application/json" ], @@ -685,7 +719,12 @@ }, "/api/batches/{pickup_code}": { "get": { - "description": "根据取件码获取文件批次详详情和文件列表", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "根据取件码获取文件批次详细信息和文件列表。可选提供带 pickup scope 的 API Token。", "produces": [ "application/json" ], @@ -732,7 +771,12 @@ }, "/api/batches/{pickup_code}/download": { "get": { - "description": "根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载。可选提供带 pickup scope 的 API Token。", "produces": [ "application/zip" ], @@ -799,7 +843,12 @@ }, "/api/files/{file_id}/download": { "get": { - "description": "根据文件 ID 下载单个文件", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。", "produces": [ "application/octet-stream" ], @@ -933,13 +982,16 @@ "config.APITokenConfig": { "type": "object", "properties": { - "allowAdminAPI": { + "allow_admin_api": { + "description": "是否允许 API Token 访问管理接口", "type": "boolean" }, "enabled": { + "description": "是否启用 API Token", "type": "boolean" }, - "maxTokens": { + "max_tokens": { + "description": "最大 Token 数量", "type": "integer" } } @@ -947,23 +999,53 @@ "config.Config": { "type": "object", "properties": { - "apitoken": { - "$ref": "#/definitions/config.APITokenConfig" + "api_token": { + "description": "API Token 设置", + "allOf": [ + { + "$ref": "#/definitions/config.APITokenConfig" + } + ] }, "database": { - "$ref": "#/definitions/config.DatabaseConfig" + "description": "数据库设置", + "allOf": [ + { + "$ref": "#/definitions/config.DatabaseConfig" + } + ] }, "security": { - "$ref": "#/definitions/config.SecurityConfig" + "description": "安全设置", + "allOf": [ + { + "$ref": "#/definitions/config.SecurityConfig" + } + ] }, "site": { - "$ref": "#/definitions/config.SiteConfig" + "description": "站点设置", + "allOf": [ + { + "$ref": "#/definitions/config.SiteConfig" + } + ] }, "storage": { - "$ref": "#/definitions/config.StorageConfig" + "description": "存储设置", + "allOf": [ + { + "$ref": "#/definitions/config.StorageConfig" + } + ] }, "upload": { - "$ref": "#/definitions/config.UploadConfig" + "description": "上传设置", + "allOf": [ + { + "$ref": "#/definitions/config.UploadConfig" + } + ] } } }, @@ -971,6 +1053,7 @@ "type": "object", "properties": { "path": { + "description": "数据库文件路径", "type": "string" } } @@ -978,16 +1061,20 @@ "config.SecurityConfig": { "type": "object", "properties": { - "adminPasswordHash": { + "admin_password_hash": { + "description": "管理员密码哈希 (bcrypt)", "type": "string" }, - "jwtsecret": { + "jwt_secret": { + "description": "JWT 签名密钥", "type": "string" }, - "pickupCodeLength": { + "pickup_code_length": { + "description": "取件码长度", "type": "integer" }, - "pickupFailLimit": { + "pickup_fail_limit": { + "description": "取件失败尝试限制", "type": "integer" } } @@ -996,9 +1083,11 @@ "type": "object", "properties": { "description": { + "description": "站点描述", "type": "string" }, "name": { + "description": "站点名称", "type": "string" } } @@ -1010,6 +1099,7 @@ "type": "object", "properties": { "path": { + "description": "本地存储路径", "type": "string" } } @@ -1017,42 +1107,53 @@ "s3": { "type": "object", "properties": { - "accessKey": { + "access_key": { + "description": "S3 Access Key", "type": "string" }, "bucket": { + "description": "S3 Bucket", "type": "string" }, "endpoint": { + "description": "S3 端点", "type": "string" }, "region": { + "description": "S3 区域", "type": "string" }, - "secretKey": { + "secret_key": { + "description": "S3 Secret Key", "type": "string" }, - "useSSL": { + "use_ssl": { + "description": "是否使用 SSL", "type": "boolean" } } }, "type": { + "description": "存储类型: local, webdav, s3", "type": "string" }, - "webDAV": { + "webdav": { "type": "object", "properties": { "password": { + "description": "WebDAV 密码", "type": "string" }, "root": { + "description": "WebDAV 根目录", "type": "string" }, "url": { + "description": "WebDAV 地址", "type": "string" }, "username": { + "description": "WebDAV 用户名", "type": "string" } } @@ -1062,14 +1163,21 @@ "config.UploadConfig": { "type": "object", "properties": { - "maxBatchFiles": { + "max_batch_files": { + "description": "每个批次最大文件数", "type": "integer" }, - "maxFileSizeMB": { + "max_file_size_mb": { + "description": "单个文件最大大小 (MB)", "type": "integer" }, - "maxRetentionDays": { + "max_retention_days": { + "description": "最大保留天数", "type": "integer" + }, + "require_token": { + "description": "是否强制要求上传 Token", + "type": "boolean" } } }, @@ -1221,25 +1329,50 @@ } } }, + "public.PublicAPITokenConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "public.PublicConfig": { "type": "object", "properties": { "api_token": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - } - } + "$ref": "#/definitions/public.PublicAPITokenConfig" + }, + "security": { + "$ref": "#/definitions/public.PublicSecurityConfig" }, "site": { "$ref": "#/definitions/config.SiteConfig" }, + "storage": { + "$ref": "#/definitions/public.PublicStorageConfig" + }, "upload": { "$ref": "#/definitions/config.UploadConfig" } } }, + "public.PublicSecurityConfig": { + "type": "object", + "properties": { + "pickup_code_length": { + "type": "integer" + } + } + }, + "public.PublicStorageConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, "public.UploadResponse": { "type": "object", "properties": { @@ -1284,8 +1417,14 @@ } }, "securityDefinitions": { + "APITokenAuth": { + "description": "Type \"Bearer \u003cAPI-Token\u003e\" to authenticate. Required scope depends on the endpoint.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, "AdminAuth": { - "description": "Type \"Bearer \u003cyour-jwt-token\u003e\" to authenticate.", + "description": "Type \"Bearer \u003cJWT-Token\u003e\" or \"Bearer \u003cAPI-Token\u003e\" to authenticate. API Token must have 'admin' scope.", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e1e334e..1e98cbb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -61,49 +61,71 @@ definitions: type: object config.APITokenConfig: properties: - allowAdminAPI: + allow_admin_api: + description: 是否允许 API Token 访问管理接口 type: boolean enabled: + description: 是否启用 API Token type: boolean - maxTokens: + max_tokens: + description: 最大 Token 数量 type: integer type: object config.Config: properties: - apitoken: - $ref: '#/definitions/config.APITokenConfig' + api_token: + allOf: + - $ref: '#/definitions/config.APITokenConfig' + description: API Token 设置 database: - $ref: '#/definitions/config.DatabaseConfig' + allOf: + - $ref: '#/definitions/config.DatabaseConfig' + description: 数据库设置 security: - $ref: '#/definitions/config.SecurityConfig' + allOf: + - $ref: '#/definitions/config.SecurityConfig' + description: 安全设置 site: - $ref: '#/definitions/config.SiteConfig' + allOf: + - $ref: '#/definitions/config.SiteConfig' + description: 站点设置 storage: - $ref: '#/definitions/config.StorageConfig' + allOf: + - $ref: '#/definitions/config.StorageConfig' + description: 存储设置 upload: - $ref: '#/definitions/config.UploadConfig' + allOf: + - $ref: '#/definitions/config.UploadConfig' + description: 上传设置 type: object config.DatabaseConfig: properties: path: + description: 数据库文件路径 type: string type: object config.SecurityConfig: properties: - adminPasswordHash: + admin_password_hash: + description: 管理员密码哈希 (bcrypt) type: string - jwtsecret: + jwt_secret: + description: JWT 签名密钥 type: string - pickupCodeLength: + pickup_code_length: + description: 取件码长度 type: integer - pickupFailLimit: + pickup_fail_limit: + description: 取件失败尝试限制 type: integer type: object config.SiteConfig: properties: description: + description: 站点描述 type: string name: + description: 站点名称 type: string type: object config.StorageConfig: @@ -111,45 +133,63 @@ definitions: local: properties: path: + description: 本地存储路径 type: string type: object s3: properties: - accessKey: + access_key: + description: S3 Access Key type: string bucket: + description: S3 Bucket type: string endpoint: + description: S3 端点 type: string region: + description: S3 区域 type: string - secretKey: + secret_key: + description: S3 Secret Key type: string - useSSL: + use_ssl: + description: 是否使用 SSL type: boolean type: object type: + description: '存储类型: local, webdav, s3' type: string - webDAV: + webdav: properties: password: + description: WebDAV 密码 type: string root: + description: WebDAV 根目录 type: string url: + description: WebDAV 地址 type: string username: + description: WebDAV 用户名 type: string type: object type: object config.UploadConfig: properties: - maxBatchFiles: + max_batch_files: + description: 每个批次最大文件数 type: integer - maxFileSizeMB: + max_file_size_mb: + description: 单个文件最大大小 (MB) type: integer - maxRetentionDays: + max_retention_days: + description: 最大保留天数 type: integer + require_token: + description: 是否强制要求上传 Token + type: boolean type: object model.APIToken: properties: @@ -250,18 +290,34 @@ definitions: type: type: string type: object + public.PublicAPITokenConfig: + properties: + enabled: + type: boolean + type: object public.PublicConfig: properties: api_token: - properties: - enabled: - type: boolean - type: object + $ref: '#/definitions/public.PublicAPITokenConfig' + security: + $ref: '#/definitions/public.PublicSecurityConfig' site: $ref: '#/definitions/config.SiteConfig' + storage: + $ref: '#/definitions/public.PublicStorageConfig' upload: $ref: '#/definitions/config.UploadConfig' type: object + public.PublicSecurityConfig: + properties: + pickup_code_length: + type: integer + type: object + public.PublicStorageConfig: + properties: + type: + type: string + type: object public.UploadResponse: properties: batch_id: @@ -306,7 +362,7 @@ info: paths: /admin/api-tokens: get: - description: 获取系统中所有 API Token 的详详信息(不包含哈希) + description: 获取系统中所有 API Token 的详细信息(不包含哈希) produces: - application/json responses: @@ -553,7 +609,12 @@ paths: "200": description: OK schema: - $ref: '#/definitions/config.Config' + allOf: + - $ref: '#/definitions/model.Response' + - properties: + data: + $ref: '#/definitions/config.Config' + type: object security: - AdminAuth: [] summary: 获取完整配置 @@ -576,7 +637,12 @@ paths: "200": description: OK schema: - $ref: '#/definitions/model.Response' + allOf: + - $ref: '#/definitions/model.Response' + - properties: + data: + $ref: '#/definitions/config.Config' + type: object "400": description: Bad Request schema: @@ -625,7 +691,8 @@ paths: post: consumes: - multipart/form-data - description: 上传一个或多个文件并创建一个提取批次 + description: 上传一个或多个文件并创建一个提取批次。如果配置了 require_token,则必须提供带 upload scope 的 API + Token。 parameters: - description: 文件列表 in: formData @@ -668,12 +735,14 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/model.Response' + security: + - APITokenAuth: [] summary: 上传文件 tags: - Public /api/batches/{pickup_code}: get: - description: 根据取件码获取文件批次详详情和文件列表 + description: 根据取件码获取文件批次详细信息和文件列表。可选提供带 pickup scope 的 API Token。 parameters: - description: 取件码 in: path @@ -696,12 +765,14 @@ paths: description: Not Found schema: $ref: '#/definitions/model.Response' + security: + - APITokenAuth: [] summary: 获取批次信息 tags: - Public /api/batches/{pickup_code}/download: get: - description: 根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载 + description: 根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载。可选提供带 pickup scope 的 API Token。 parameters: - description: 取件码 in: path @@ -719,6 +790,8 @@ paths: description: Not Found schema: $ref: '#/definitions/model.Response' + security: + - APITokenAuth: [] summary: 批量下载文件 tags: - Public @@ -726,7 +799,8 @@ paths: post: consumes: - application/json - description: 中转一段长文本内容并创建一个提取批次 + description: 中转一段长文本内容并创建一个提取批次。如果配置了 require_token,则必须提供带 upload scope 的 API + Token。 parameters: - description: 文本内容及配置 in: body @@ -754,6 +828,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/model.Response' + security: + - APITokenAuth: [] summary: 发送长文本 tags: - Public @@ -777,7 +853,7 @@ paths: - Public /api/files/{file_id}/download: get: - description: 根据文件 ID 下载单个文件 + description: 根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。 parameters: - description: 文件 ID (UUID) in: path @@ -799,12 +875,21 @@ paths: description: Gone schema: $ref: '#/definitions/model.Response' + security: + - APITokenAuth: [] summary: 下载单个文件 tags: - Public securityDefinitions: + APITokenAuth: + description: Type "Bearer " to authenticate. Required scope depends + on the endpoint. + in: header + name: Authorization + type: apiKey AdminAuth: - description: Type "Bearer " to authenticate. + description: Type "Bearer " or "Bearer " to authenticate. + API Token must have 'admin' scope. in: header name: Authorization type: apiKey diff --git a/internal/api/admin/config.go b/internal/api/admin/config.go index 8269d07..ffa521b 100644 --- a/internal/api/admin/config.go +++ b/internal/api/admin/config.go @@ -21,10 +21,10 @@ func NewConfigHandler() *ConfigHandler { // @Tags Admin // @Security AdminAuth // @Produce json -// @Success 200 {object} config.Config +// @Success 200 {object} model.Response{data=config.Config} // @Router /admin/config [get] func (h *ConfigHandler) GetConfig(c *gin.Context) { - c.JSON(http.StatusOK, config.GlobalConfig) + c.JSON(http.StatusOK, model.SuccessResponse(config.GlobalConfig)) } // UpdateConfig 更新配置 @@ -35,7 +35,7 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) { // @Accept json // @Produce json // @Param config body config.Config true "新配置内容" -// @Success 200 {object} model.Response +// @Success 200 {object} model.Response{data=config.Config} // @Failure 400 {object} model.Response // @Failure 500 {object} model.Response // @Router /admin/config [put] @@ -66,5 +66,5 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) { return } - c.JSON(http.StatusOK, model.SuccessResponse("Config updated successfully and hot-reloaded")) + c.JSON(http.StatusOK, model.SuccessResponse(config.GlobalConfig)) } diff --git a/internal/api/admin/token.go b/internal/api/admin/token.go index 91a4a12..215db85 100644 --- a/internal/api/admin/token.go +++ b/internal/api/admin/token.go @@ -33,7 +33,7 @@ type CreateTokenResponse struct { // ListTokens 获取 API Token 列表 // @Summary 获取 API Token 列表 -// @Description 获取系统中所有 API Token 的详详信息(不包含哈希) +// @Description 获取系统中所有 API Token 的详细信息(不包含哈希) // @Tags Admin // @Security AdminAuth // @Produce json diff --git a/internal/api/middleware/auth.go b/internal/api/middleware/auth.go index 519b412..348158e 100644 --- a/internal/api/middleware/auth.go +++ b/internal/api/middleware/auth.go @@ -12,6 +12,7 @@ import ( ) func AdminAuth() gin.HandlerFunc { + tokenService := service.NewTokenService() return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { @@ -27,29 +28,41 @@ func AdminAuth() gin.HandlerFunc { return } - claims, err := auth.ParseToken(parts[1]) - if err != nil { - c.JSON(http.StatusUnauthorized, model.ErrorResponse(model.CodeUnauthorized, "Invalid or expired token")) - c.Abort() + tokenStr := parts[1] + + // 1. 尝试解析为管理员 JWT + claims, err := auth.ParseToken(tokenStr) + if err == nil { + c.Set("admin_id", claims.AdminID) + c.Next() return } - c.Set("admin_id", claims.AdminID) - c.Next() + // 2. 尝试解析为 API Token (如果配置允许) + if config.GlobalConfig.APIToken.Enabled && config.GlobalConfig.APIToken.AllowAdminAPI { + token, err := tokenService.ValidateToken(tokenStr, model.ScopeAdmin) + if err == nil { + c.Set("token_id", token.ID) + c.Set("token_scope", token.Scope) + c.Next() + return + } + } + + c.JSON(http.StatusUnauthorized, model.ErrorResponse(model.CodeUnauthorized, "Invalid or expired token")) + c.Abort() } } -func APITokenAuth(requiredScope string) gin.HandlerFunc { +func APITokenAuth(requiredScope string, optional bool) gin.HandlerFunc { tokenService := service.NewTokenService() return func(c *gin.Context) { - if !config.GlobalConfig.APIToken.Enabled { - c.JSON(http.StatusForbidden, model.ErrorResponse(model.CodeForbidden, "API Token is disabled")) - c.Abort() - return - } - authHeader := c.GetHeader("Authorization") if authHeader == "" { + if optional { + c.Next() + return + } c.JSON(http.StatusUnauthorized, model.ErrorResponse(model.CodeUnauthorized, "Authorization header required")) c.Abort() return @@ -57,13 +70,31 @@ func APITokenAuth(requiredScope string) gin.HandlerFunc { parts := strings.SplitN(authHeader, " ", 2) if !(len(parts) == 2 && parts[0] == "Bearer") { + if optional { + c.Next() + return + } c.JSON(http.StatusUnauthorized, model.ErrorResponse(model.CodeUnauthorized, "Invalid authorization format")) c.Abort() return } + if !config.GlobalConfig.APIToken.Enabled { + if optional { + c.Next() + return + } + c.JSON(http.StatusForbidden, model.ErrorResponse(model.CodeForbidden, "API Token is disabled")) + c.Abort() + return + } + token, err := tokenService.ValidateToken(parts[1], requiredScope) if err != nil { + if optional { + c.Next() + return + } c.JSON(http.StatusUnauthorized, model.ErrorResponse(model.CodeUnauthorized, err.Error())) c.Abort() return diff --git a/internal/api/public/config.go b/internal/api/public/config.go index bd1ede2..1493245 100644 --- a/internal/api/public/config.go +++ b/internal/api/public/config.go @@ -16,11 +16,23 @@ func NewConfigHandler() *ConfigHandler { // PublicConfig 公开配置结构 type PublicConfig struct { - Site config.SiteConfig `json:"site"` - Upload config.UploadConfig `json:"upload"` - APIToken struct { - Enabled bool `json:"enabled"` - } `json:"api_token"` + Site config.SiteConfig `json:"site"` + Security PublicSecurityConfig `json:"security"` + Upload config.UploadConfig `json:"upload"` + APIToken PublicAPITokenConfig `json:"api_token"` + Storage PublicStorageConfig `json:"storage"` +} + +type PublicSecurityConfig struct { + PickupCodeLength int `json:"pickup_code_length"` +} + +type PublicAPITokenConfig struct { + Enabled bool `json:"enabled"` +} + +type PublicStorageConfig struct { + Type string `json:"type"` } // GetPublicConfig 获取非敏感配置 @@ -32,10 +44,18 @@ type PublicConfig struct { // @Router /api/config [get] func (h *ConfigHandler) GetPublicConfig(c *gin.Context) { pub := PublicConfig{ - Site: config.GlobalConfig.Site, + Site: config.GlobalConfig.Site, + Security: PublicSecurityConfig{ + PickupCodeLength: config.GlobalConfig.Security.PickupCodeLength, + }, Upload: config.GlobalConfig.Upload, + APIToken: PublicAPITokenConfig{ + Enabled: config.GlobalConfig.APIToken.Enabled, + }, + Storage: PublicStorageConfig{ + Type: config.GlobalConfig.Storage.Type, + }, } - pub.APIToken.Enabled = config.GlobalConfig.APIToken.Enabled c.JSON(http.StatusOK, model.SuccessResponse(pub)) } diff --git a/internal/api/public/pickup.go b/internal/api/public/pickup.go index 2d4aaf2..6ea7e29 100644 --- a/internal/api/public/pickup.go +++ b/internal/api/public/pickup.go @@ -29,8 +29,9 @@ type PickupResponse struct { // DownloadBatch 批量下载文件 (ZIP) // @Summary 批量下载文件 -// @Description 根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载 +// @Description 根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载。可选提供带 pickup scope 的 API Token。 // @Tags Public +// @Security APITokenAuth // @Param pickup_code path string true "取件码" // @Produce application/zip // @Success 200 {file} file @@ -82,8 +83,9 @@ func NewPickupHandler() *PickupHandler { // Pickup 获取批次信息 // @Summary 获取批次信息 -// @Description 根据取件码获取文件批次详详情和文件列表 +// @Description 根据取件码获取文件批次详细信息和文件列表。可选提供带 pickup scope 的 API Token。 // @Tags Public +// @Security APITokenAuth // @Produce json // @Param pickup_code path string true "取件码" // @Success 200 {object} model.Response{data=PickupResponse} @@ -122,8 +124,9 @@ func (h *PickupHandler) Pickup(c *gin.Context) { // DownloadFile 下载单个文件 // @Summary 下载单个文件 -// @Description 根据文件 ID 下载单个文件 +// @Description 根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。 // @Tags Public +// @Security APITokenAuth // @Param file_id path string true "文件 ID (UUID)" // @Produce application/octet-stream // @Success 200 {file} file diff --git a/internal/api/public/upload.go b/internal/api/public/upload.go index 8f13683..9495cb5 100644 --- a/internal/api/public/upload.go +++ b/internal/api/public/upload.go @@ -29,10 +29,11 @@ type UploadResponse struct { // Upload 上传文件并生成取件码 // @Summary 上传文件 -// @Description 上传一个或多个文件并创建一个提取批次 +// @Description 上传一个或多个文件并创建一个提取批次。如果配置了 require_token,则必须提供带 upload scope 的 API Token。 // @Tags Public // @Accept multipart/form-data // @Produce json +// @Security APITokenAuth // @Param files formData file true "文件列表" // @Param remark formData string false "备注" // @Param expire_type formData string false "过期类型 (time/download/permanent)" @@ -105,10 +106,11 @@ type UploadTextRequest struct { // UploadText 发送长文本并生成取件码 // @Summary 发送长文本 -// @Description 中转一段长文本内容并创建一个提取批次 +// @Description 中转一段长文本内容并创建一个提取批次。如果配置了 require_token,则必须提供带 upload scope 的 API Token。 // @Tags Public // @Accept json // @Produce json +// @Security APITokenAuth // @Param request body UploadTextRequest true "文本内容及配置" // @Success 200 {object} model.Response{data=UploadResponse} // @Failure 400 {object} model.Response diff --git a/internal/config/config.go b/internal/config/config.go index 720a95a..3956267 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,61 +8,62 @@ import ( ) type Config struct { - Site SiteConfig `yaml:"site"` - Security SecurityConfig `yaml:"security"` - Upload UploadConfig `yaml:"upload"` - Storage StorageConfig `yaml:"storage"` - APIToken APITokenConfig `yaml:"api_token"` - Database DatabaseConfig `yaml:"database"` + Site SiteConfig `yaml:"site" json:"site"` // 站点设置 + Security SecurityConfig `yaml:"security" json:"security"` // 安全设置 + Upload UploadConfig `yaml:"upload" json:"upload"` // 上传设置 + Storage StorageConfig `yaml:"storage" json:"storage"` // 存储设置 + APIToken APITokenConfig `yaml:"api_token" json:"api_token"` // API Token 设置 + Database DatabaseConfig `yaml:"database" json:"database"` // 数据库设置 } type SiteConfig struct { - Name string `yaml:"name"` - Description string `yaml:"description"` + Name string `yaml:"name" json:"name"` // 站点名称 + Description string `yaml:"description" json:"description"` // 站点描述 } type SecurityConfig struct { - AdminPasswordHash string `yaml:"admin_password_hash"` - PickupCodeLength int `yaml:"pickup_code_length"` - PickupFailLimit int `yaml:"pickup_fail_limit"` - JWTSecret string `yaml:"jwt_secret"` + AdminPasswordHash string `yaml:"admin_password_hash" json:"admin_password_hash"` // 管理员密码哈希 (bcrypt) + PickupCodeLength int `yaml:"pickup_code_length" json:"pickup_code_length"` // 取件码长度 + PickupFailLimit int `yaml:"pickup_fail_limit" json:"pickup_fail_limit"` // 取件失败尝试限制 + JWTSecret string `yaml:"jwt_secret" json:"jwt_secret"` // JWT 签名密钥 } type UploadConfig struct { - MaxFileSizeMB int64 `yaml:"max_file_size_mb"` - MaxBatchFiles int `yaml:"max_batch_files"` - MaxRetentionDays int `yaml:"max_retention_days"` + MaxFileSizeMB int64 `yaml:"max_file_size_mb" json:"max_file_size_mb"` // 单个文件最大大小 (MB) + MaxBatchFiles int `yaml:"max_batch_files" json:"max_batch_files"` // 每个批次最大文件数 + MaxRetentionDays int `yaml:"max_retention_days" json:"max_retention_days"` // 最大保留天数 + RequireToken bool `yaml:"require_token" json:"require_token"` // 是否强制要求上传 Token } type StorageConfig struct { - Type string `yaml:"type"` + Type string `yaml:"type" json:"type"` // 存储类型: local, webdav, s3 Local struct { - Path string `yaml:"path"` - } `yaml:"local"` + Path string `yaml:"path" json:"path"` // 本地存储路径 + } `yaml:"local" json:"local"` WebDAV struct { - URL string `yaml:"url"` - Username string `yaml:"username"` - Password string `yaml:"password"` - Root string `yaml:"root"` - } `yaml:"webdav"` + URL string `yaml:"url" json:"url"` // WebDAV 地址 + Username string `yaml:"username" json:"username"` // WebDAV 用户名 + Password string `yaml:"password" json:"password"` // WebDAV 密码 + Root string `yaml:"root" json:"root"` // WebDAV 根目录 + } `yaml:"webdav" json:"webdav"` S3 struct { - Endpoint string `yaml:"endpoint"` - Region string `yaml:"region"` - AccessKey string `yaml:"access_key"` - SecretKey string `yaml:"secret_key"` - Bucket string `yaml:"bucket"` - UseSSL bool `yaml:"use_ssl"` - } `yaml:"s3"` + Endpoint string `yaml:"endpoint" json:"endpoint"` // S3 端点 + Region string `yaml:"region" json:"region"` // S3 区域 + AccessKey string `yaml:"access_key" json:"access_key"` // S3 Access Key + SecretKey string `yaml:"secret_key" json:"secret_key"` // S3 Secret Key + Bucket string `yaml:"bucket" json:"bucket"` // S3 Bucket + UseSSL bool `yaml:"use_ssl" json:"use_ssl"` // 是否使用 SSL + } `yaml:"s3" json:"s3"` } type APITokenConfig struct { - Enabled bool `yaml:"enabled"` - AllowAdminAPI bool `yaml:"allow_admin_api"` - MaxTokens int `yaml:"max_tokens"` + Enabled bool `yaml:"enabled" json:"enabled"` // 是否启用 API Token + AllowAdminAPI bool `yaml:"allow_admin_api" json:"allow_admin_api"` // 是否允许 API Token 访问管理接口 + MaxTokens int `yaml:"max_tokens" json:"max_tokens"` // 最大 Token 数量 } type DatabaseConfig struct { - Path string `yaml:"path"` + Path string `yaml:"path" json:"path"` // 数据库文件路径 } var ( diff --git a/internal/model/api_token.go b/internal/model/api_token.go index 459dcc8..1318c9c 100644 --- a/internal/model/api_token.go +++ b/internal/model/api_token.go @@ -4,6 +4,12 @@ import ( "time" ) +const ( + ScopeUpload = "upload" // 上传权限 + ScopePickup = "pickup" // 取件/下载权限 + ScopeAdmin = "admin" // 管理权限 +) + type APIToken struct { ID uint `gorm:"primaryKey" json:"id"` Name string `json:"name"` diff --git a/main.go b/main.go index 744b6de..5b5eead 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "FileRelay/internal/api/public" "FileRelay/internal/bootstrap" "FileRelay/internal/config" + "FileRelay/internal/model" "FileRelay/internal/task" "context" "fmt" @@ -35,7 +36,12 @@ import ( // @securityDefinitions.apikey AdminAuth // @in header // @name Authorization -// @description Type "Bearer " to authenticate. +// @description Type "Bearer " or "Bearer " to authenticate. API Token must have 'admin' scope. + +// @securityDefinitions.apikey APITokenAuth +// @in header +// @name Authorization +// @description Type "Bearer " to authenticate. Required scope depends on the endpoint. func main() { // 1. 加载配置 @@ -72,12 +78,12 @@ func main() { { api.GET("/config", publicConfigHandler.GetPublicConfig) // 统一使用 /batches 作为资源路径 - api.POST("/batches", uploadHandler.Upload) - api.POST("/batches/text", uploadHandler.UploadText) - api.GET("/batches/:pickup_code", middleware.PickupRateLimit(), pickupHandler.Pickup) - api.GET("/batches/:pickup_code/download", pickupHandler.DownloadBatch) + api.POST("/batches", middleware.APITokenAuth(model.ScopeUpload, !config.GlobalConfig.Upload.RequireToken), uploadHandler.Upload) + api.POST("/batches/text", middleware.APITokenAuth(model.ScopeUpload, !config.GlobalConfig.Upload.RequireToken), uploadHandler.UploadText) + api.GET("/batches/:pickup_code", middleware.PickupRateLimit(), middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.Pickup) + api.GET("/batches/:pickup_code/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadBatch) // 文件下载保持 /files/:id/download 风格 - api.GET("/files/:file_id/download", pickupHandler.DownloadFile) + api.GET("/files/:file_id/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadFile) // 保持旧路由兼容性 (可选,但为了平滑过渡通常建议保留一段时间或直接更新) // 这里根据需求“调整不符合规范的”,我将直接采用新路由