diff --git a/.env_sample b/.env_sample index ce065f7..807d34a 100644 --- a/.env_sample +++ b/.env_sample @@ -4,8 +4,11 @@ #端口 PORT=3000 -#飞书机器人Webhook地址,用于接收相关通知 +#飞书机器人Webhook地址/应用密钥,用于接收相关通知 LARK_WEBHOOK_URL= +LARK_APP_ID= +LARK_APP_SECRET= +LARK_REDIRECT_URI=http://localhost:9000/oauth/callback #Gitea相关配置,注意后边要带/api/v1 GITEA_BASE_URL=https://git.langcore.net/api/v1 diff --git a/.gitignore b/.gitignore index f089a17..e94f5f1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ logs/ .idea/ .vscode/ .cache/ -sync_database.sqlite \ No newline at end of file +sync_database.sqlite +larkbot.md \ No newline at end of file diff --git a/data/larkRules.json b/data/larkRules.json new file mode 100644 index 0000000..b2f4aa3 --- /dev/null +++ b/data/larkRules.json @@ -0,0 +1,32 @@ +{ + "rules": [ + { + "id": "589d9419-00e1-4628-adbb-0582d35a8e9a", + "createdAt": "2026-02-02T05:24:04.963Z", + "enabled": true, + "name": "里程碑", + "event": "issue.milestoned", + "channel": "group", + "atUsers": [ + "13397559898" + ], + "filterKey": null, + "filterValue": null, + "updatedAt": "2026-02-02T05:31:16.958Z" + }, + { + "id": "df9f9646-a6f5-40cc-8434-9da677af5f86", + "createdAt": "2026-02-02T05:29:44.294Z", + "enabled": true, + "name": "标签", + "event": "issue.label_updated", + "channel": "group", + "atUsers": [ + "13397559898" + ], + "filterKey": "label", + "filterValue": "类型/Bug", + "updatedAt": "2026-02-02T05:31:20.010Z" + } + ] +} \ No newline at end of file diff --git a/mappings.json b/data/mappings.json similarity index 97% rename from mappings.json rename to data/mappings.json index 1935770..93b3f18 100644 --- a/mappings.json +++ b/data/mappings.json @@ -21,7 +21,7 @@ "loren/SyncbotPlayground": { "jira": { "projectId": "10600", - "projectKey": "TEST", + "projectKey": "TASKBOT", "sprintField": "customfield_10105" }, "priorities": { @@ -33,8 +33,8 @@ }, "types": { "类型/故事": "10100", - "类型/Bug": "10004", - "类型/任务": "10100" + "类型/任务": "10100", + "类型/Bug": "10004" }, "transitions": { "close": "31", diff --git a/index.js b/index.js index 78a0e04..b9dc982 100644 --- a/index.js +++ b/index.js @@ -6,9 +6,12 @@ const config = require('./src/config/env'); const { getConfiguredRepos } = require('./src/config/mappings'); const { handleIssueEvent } = require('./src/logic/syncManager'); const { handleJiraHook } = require('./src/logic/jiraSyncManager'); +const { notify, inferEventType } = require('./src/logic/larkNotifier'); const editorRoutes = require('./src/routes/control'); const logger = require('./src/utils/logger'); +const { larkAuthMiddleware, loginHandler, callbackHandler } = require('./src/middleware/larkAuth'); + const app = new Hono(); const requestCounts = new Map(); @@ -47,29 +50,25 @@ setInterval(() => { } }, 5 * 60 * 1000); -//内网访问控制中间件:保护管理界面,只允许dotenv配置的域名访问 -const internalOnlyMiddleware = async (c, next) => { - const pathname = new URL(c.req.url).pathname; +//鉴权路由 +app.get('/login', loginHandler); +app.get('/oauth/callback', callbackHandler); - if (pathname.startsWith('/hooks/')) { - return await next(); - } +// 全局鉴权中间件 (飞书 OAuth) +app.use('*', larkAuthMiddleware); - const host = (c.req.header('host') || '').split(':')[0]; - const allowedHosts = config.app.dashboardAllowedHosts; +// 获取当前用户信息 +const { meHandler, adminMiddleware } = require('./src/middleware/larkAuth'); +app.get('/api/me', meHandler); - if (!allowedHosts.some(allowed => host === allowed || host.endsWith('.' + allowed))) { - logger.security(`Blocked access from unauthorized host: ${host}`, { - path: pathname, - ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown' - }); - return c.text('Forbidden - Access denied from this domain', 403); - } - - await next(); -}; - -app.use('*', internalOnlyMiddleware); +// 敏感接口保护 (仅管理员可访问) +// 注意:Hono 的中间件匹配顺序很重要,specific routes should be handled or protected explicitly +app.use('/api/env', adminMiddleware); +app.use('/api/restart', adminMiddleware); +app.use('/api/logs', adminMiddleware); +app.use('/api/logs/clear', adminMiddleware); +app.use('/api/history', adminMiddleware); +app.use('/api/status', adminMiddleware); //Gitea webhook处理入口 app.post('/hooks/gitea', rateLimiter, async (c) => { @@ -100,16 +99,31 @@ app.post('/hooks/gitea', rateLimiter, async (c) => { //解析JSON const body = JSON.parse(rawBody); - //Payload结构验证 - if (!body || !body.issue || !body.repository) { - logger.warn('Invalid payload structure', { ip }); - return c.json({ error: 'Invalid payload structure' }, 400); + //触发飞书通知(所有事件类型) + const eventType = inferEventType(event, body); + if (eventType) { + notify(eventType, body).catch(err => + logger.error('[LarkNotifier] Async notify error:', err.message) + ); } + //Issue 事件处理(原有逻辑) if (event === 'issues' || event === 'issue_comment') { - //异步处理,不阻塞webhook返回 - handleIssueEvent(body).catch(err => logger.error('Async handler error', err.message)); - } else { + if (body.issue && body.repository) { + handleIssueEvent(body).catch(err => logger.error('Async handler error', err.message)); + } else { + logger.warn('Invalid payload structure for issue event', { ip }); + } + } + //PR 事件 - 仅记录日志(未来可扩展) + else if (event === 'pull_request') { + logger.info(`[PR Event] ${body.action} - ${body.pull_request?.title || 'Unknown'}`, { ip }); + } + //Release 事件 - 仅记录日志(未来可扩展) + else if (event === 'release') { + logger.info(`[Release Event] ${body.action} - ${body.release?.name || 'Unknown'}`, { ip }); + } + else { logger.info(`Ignored event type: ${event}`, { ip }); } @@ -146,6 +160,7 @@ const configuredRepos = getConfiguredRepos(); //控制台首页 app.get('/', (c) => c.redirect('/dashboard')); app.get('/dashboard', serveStatic({ path: './public/dashboard.html' })); +app.get('/error.html', serveStatic({ path: './public/error.html' })); app.route('/api', editorRoutes); app.route('/editor/api', editorRoutes); diff --git a/larkauth.md b/larkauth.md new file mode 100644 index 0000000..6017330 --- /dev/null +++ b/larkauth.md @@ -0,0 +1,487 @@ +# 获取授权码 + +本接口用于发起用户授权,应用在用户同意授权后将获得授权码 `code`。请注意授权码的有效期为 5 分钟,且只能被使用一次。 + +## 注意事项 + +- 本接口实际为授权页面,适用于网页应用的授权场景。在需要用户授权时,应用应将用户重定向至本授权页面。当用户在授权页面点击授权后(在飞书客户端内打开网页应用时可免确认直接跳转),浏览器将跳转至 `redirect_uri` 所指定的地址,并携带 `code` 查询参数(即授权码)。 + +- 开发者可通过授权码获取 `user_access_token`,以调用 OpenAPI(例如[获取用户信息](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/user_info/get))。有关获取 `user_access_token` 的详细步骤,可参考[获取 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)。 +- 通过本接口配合[获取 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)以及[获取用户信息](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/user_info/get),应用可实现飞书授权登录。 +- 完整的用户授权链路实现,可参考[浏览器应用接入指南](https://open.feishu.cn/document/common-capabilities/sso/web-application-end-user-consent/guide)。 +- 在打开授权页面时,需要通过拼接 `scope` 查询参数声明应用所需的用户授权权限。例如,获取通讯录基本信息的权限对应的 `scope` 键为 `contact:contact.base:readonly`。 +- 用户授予应用的权限是累积的,最新生成的 `user_access_token` 将包含用户历史上已授予的所有权限。 +- 当应用使用 `user_access_token` 调用某个 OpenAPI 时,必须确保该 `user_access_token` 具备[目标 OpenAPI 所需的权限](https://open.feishu.cn/document/ukTMukTMukTM/uQjN3QjL0YzN04CN2cDN),否则调用将失败。 + +## 请求 + +基本 |   +---|--- +HTTP URL | https://accounts.feishu.cn/open-apis/authen/v1/authorize +HTTP Method | GET +接口频率限制 | 1000 次/分钟、50 次/秒 +支持的应用类型 | Custom App、Store App +权限要求
**调用该 API 所需的权限。开启其中任意一项权限即可调用** | 无 + +### 查询参数 + +> 为了确保 URL 构造 & 编码正确,建议使用相关的 URL 标准库来完成 URL 的解析和构建,避免手动拼接。 + +名称 | 类型 | 必填 | 描述 +---|---|---|--- +client_id | string | 是 | 应用的 App ID,可以在开发者后台的**凭证与基础信息**页面查看 App ID。有关 App ID 的详细介绍,请参考[通用参数](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/terminology)。
**示例值:** `cli_a5d611352af9d00b` +response_type | string | 是 | 应用通知授权服务器所需的授权类型,对于授权码流程,固定值`code`
**示例值:** `code` +redirect_uri | string | 是 | 应用重定向地址,在用户授权成功后会跳转至该地址,同时会携带 `code` 以及 `state` 参数(如有传递 `state` 参数)。
请注意:
1. 该地址需经过 URL 编码;
2. 调用本接口前,你需要在开发者后台应用的**安全设置**页面,将用于接受 OAuth 回调的 HTTP GET 请求接口地址配置为应用的重定向 URL。重定向 URL 支持配置多个,只有在重定向 URL 列表中的 URL 才会通过开放平台的安全校验。详情请参考[配置重定向域名](https://open.feishu.cn/document/uYjL24iN/uYjN3QjL2YzN04iN2cDN)。
**示例值:** `https://example.com/api/oauth/callback` +scope | string | 否 | 用户需要增量授予应用的权限。
**格式要求:** `scope` 参数为空格分隔,区分大小写的字符串。
**注意**:
- 开发者需要根据业务场景,在[开发者后台](https://open.larkoffice.com/app)的 **权限管理** 模块中完成调用 OpenAPI 所需的 `scope` 申请后,自主拼接 `scope` 参数。如果没有在应用后台为应用申请相应权限,则实际使用应用时用户会遇到 20027 报错。
- 应用最多一次可以请求用户授予 50 个 `scope`。详情参考 [API 权限列表](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/scope-list)。
- 如果后续需要获取 `refresh_token`,此处需要添加 `offline_access` 权限。详情参考 [刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token)):
offline_access(offline_access)
**示例值:** `contact:contact bitable:app:readonly` +state | string | 否 | 用来维护请求和回调之间状态的附加字符串,在授权完成回调时会原样回传此参数。应用可以根据此字符串来判断上下文关系,同时该参数也可以用以防止 CSRF 攻击,请务必校验 `state` 参数前后是否一致。
**示例值:** `RANDOMSTRING` +code_challenge | string | 否 | 用于通过 PKCE(Proof Key for Code Exchange)流程增强授权码的安全性。
**示例值:** `E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM`
有关 PKCE 的详细信息,请参阅[RFC-7636 - Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636)。 +code_challenge_method | string | 否 | 生成 `code_challenge` 的方法。
**可选值**:
1. **`S256`**(推荐):
使用 SHA-256 哈希算法计算 `code_verifier` 的哈希值,并将结果进行 Base64URL 编码,生成 `code_challenge`。
2. **`plain`**(默认值):
直接将 `code_verifier` 作为 `code_challenge`,无需进行额外处理。
以上 `code_verifier` 是指在发起授权前,本地生成的随机字符串。 + +### 请求示例 + +> 注意仅为示例请求 URL,请根据前文描述将其中的查询参数替换为真实的值 + +``` http +https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=cli_a5d611352af9d00b&response_type=code&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Foauth%2Fcallback&scope=bitable:app:readonly%20contact:contact&state=RANDOMSTRING +``` + +## 响应 + +### 成功响应 +当用户同意授权后,浏览器将重定向至发起授权时给定的的 `redirect_uri` 地址,同时携带 `code` 和 `state` 参数。 + +名称 | 描述 +---|--- +code | 授权码,用于获取 `user_access_token`。
**字符集:** [A-Z] / [a-z] / [0-9] / "-" / "_"
**长度:** 请开发者至少预留 64 位字符
**示例值:** `2Wd5g337vo5BZXUz-3W5KECsWUmIzJ_FJ1eFD59fD1AJIibIZljTu3OLK-HP_UI1` +state | 打开授权页时传入的 `state` 参数的原值,如未传入此处不会返回。 + +示例: +```http +https://example.com/api/oauth/callback?code=2Wd5g337vo5BZXUz-3W5KECsWUmIzJ_FJ1eFD59fD1AJIibIZljTu3OLK-HP_UI1&state=RANDOMSTRING +``` + +### 失败响应 +当用户拒绝授权时,浏览器将重定向至发起授权时给定的 `redirect_uri` 地址,同时携带 `error` 和 `state` 查询参数。 当前 `error` 参数的固定值为 `access_denied`,请妥善处理拒绝授权时的情况。 + +名称 | 描述 +---|--- +error | 错误信息,当前固定为 `access_denied` +state | 打开授权页时传入的 `state` 参数的原值,如未传入此处不会返回 + +示例: +```http +https://example.com/api/oauth/callback?error=access_denied&state=RANDOMSTRING +``` + +## 常见问题 + +### 用户授权应用时报错 20027 + +**问题现象**:用户在授权应用时报错 20027 + +![image.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/8de8a8e0ce3fbe84ddabcbad2e929b5c_cUP75TqvF9.png?height=331&lazyload=true&maxWidth=600&width=792) + +**问题原因**:打开授权页时拼接的 scope 参数中包含当前应用未开通的权限。 + +**解决方案**: + +1. 确认需要用户授权的权限范围。 +2. 前往[开发者后台](https://open.feishu.cn/app),在对应应用的 **开发配置** > **权限管理** > **API 权限** 功能页开通相应的权限。具体操作参考[申请 API 权限](https://open.feishu.cn/document/ukTMukTMukTM/uQjN3QjL0YzN04CN2cDN)。 +3. 调用当前接口,自主拼接已在应用内开通的权限。 + +### 如何获取包含目标权限的 user_access_token + +在调用 OpenAPI 时,如果 `user_access_token` 缺少所需权限,将会返回以下错误: + +```json +{ + "code": 99991679, + "error": { + "log_id": "202407260711088FB107A76E0100002087", + "permission_violations": [ + { + "subject": "task:task:read", + "type": "action_privilege_required" + }, + { + "subject": "task:task:write", + "type": "action_privilege_required" + } + ] + }, + "msg": "Unauthorized. You do not have permission to perform the requested operation on the resource. Please request user re-authorization and try again. required one of these privileges: [task:task:read, task:task:write]" +} +``` + +为避免因 `user_access_token` 权限不足导致 OpenAPI 调用失败,开发者可通过 `scope` 查询参数请求用户授予相应权限,具体有以下两种方式: + +1. 一次性拼接所有需要用户授权的 `scope`,在无新增权限需求的情况下,无需重复授权。需注意单次拼接的 `scope` 数量上限为 50 个。 +2. 或者,根据 OpenAPI 调用返回的错误码及 `permission_violations` 字段,识别当前操作所需的额外权限。随后可重新生成授权链接,提示用户补充授权,并使用新生成的 `user_access_token` 继续调用 OpenAPI。 + +建议开发者遵循最小权限原则,仅要求用户授予必要的权限。 + +### redirect_uri 中带 # 时的说明 + +标准 [RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax](https://datatracker.ietf.org/doc/html/rfc3986#section-3)中约定,URI 中 `#` 后面的内容称为 fragment,位置处于 URI 最后。如果业务授权请求参数 `redirect_uri` 拼接了 `#`,授权完成后的重定向会将 `#` 和 fragment 内容拼接到 URI 最后。业务在解析获取 `code` 时需要特别注意。 + +`redirect_uri` 示例: + +``` +https://example.com/api/oauth/callback/#/login +``` + +请求示例: +``` +GET https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=cli_a5d611352af9d00b&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Foauth%2Fcallback%2F%23%2Flogin%0A&scope=bitable:app:readonly%20contact:contact&state=RANDOMSTRING +``` + +回调后浏览器地址栏中的值示例: +```shell +https://example.com/api/oauth/callback?code=2Wd5g337vo5BZXUz-3W5KECsWUmIzJ_FJ1eFD59fD1AJIibIZljTu3OLK-HP_UI1&state=RANDOMSTRING#/login +``` + +# 获取 user_access_token +OAuth 令牌接口,可用于获取 user_access_token 以及 refresh_tokenuser_access_token 为用户访问凭证,使用该凭证可以以用户身份调用 OpenAPI。refresh_token 为刷新凭证,可以用来获取新的 user_access_token。 + +- 获取 `user_access_token` 前需要先获取授权码,详见[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)。请注意授权码的有效期为 5 分钟,且只能被使用一次。 +- 用户授权时,用户必须拥有[应用的使用权限](https://open.feishu.cn/document/home/introduction-to-scope-and-authorization/availability),否则调用本接口将会报错误码 20010。 +- 获取到的 `user_access_token` 存在有效期,如何刷新 user_access_token 详见[刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token)。 +- 如果你需要获取用户信息,详见[获取用户信息](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/user_info/get)。 +**注意事项**:本接口实现遵循 [RFC 6749 - The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749) ,你可以使用[标准的 OAuth 客户端库](https://oauth.net/code/)进行接入(**推荐**) + +## 请求 + +基本 |   +---|--- +HTTP URL | https://open.feishu.cn/open-apis/authen/v2/oauth/token +HTTP Method | POST +接口频率限制 | [1000 次/分钟、50 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN) +支持的应用类型 | Custom App、Store App +权限要求
**调用该 API 所需的权限。开启其中任意一项权限即可调用
** | 无 +字段权限要求 | `refresh_token` 以及 `refresh_token_expires_in` 字段仅在具备以下权限时返回:
offline_access(offline_access) + +### 请求头 + +名称 | 类型 | 必填 | 描述 +---|---|---|--- +Content-Type | string | 是 | 请求体类型。
**固定值:**`application/json; charset=utf-8` + +### 请求体 + +名称 | 类型 | 必填 | 描述 +---|---|---|--- +grant_type | string | 是 | 授权类型。
**固定值:**`authorization_code` +client_id | string | 是 | 应用的 App ID。应用凭证 App ID 和 App Secret 获取方式:
1. 登录[飞书开发者后台](https://open.feishu.cn/app)。
2. 进入应用详情页,在左侧导航栏,单击 **凭证与基础信息**。
3. 在 **应用凭证** 区域,获取并保存 **App ID** 和 **App Secret**。
**示例值:**`cli_a5ca35a685b0x26e` +client_secret | string | 是 | 应用的 App Secret。应用凭证 App ID 和 App Secret 获取方式:
1. 登录[飞书开发者后台](https://open.feishu.cn/app)。
2. 进入应用详情页,在左侧导航栏,单击 **凭证与基础信息**。
3. 在 **应用凭证** 区域,获取并保存 **App ID** 和 **App Secret**。
**示例值:**`baBqE5um9LbFGDy3X7LcfxQX1sqpXlwy` +code | string | 是 | 授权码,详见[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)。
**示例值:**`a61hb967bd094dge949h79bbexd16dfe` +redirect_uri | string | 否 | 在构造授权页页面链接时所拼接的应用回调地址。
**示例值:**`https://example.com/api/oauth/callback` +code_verifier | string | 否 | 在发起授权前,本地生成的随机字符串,用于 PKCE(Proof Key for Code Exchange)流程。使用 PKCE 时,该值为必填项。
有关 PKCE 的详细介绍,请参阅 [RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636)。
**长度限制:** 最短 43 字符,最长 128 字符
**可用字符集:** [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
**示例值:**`TxYmzM4PHLBlqm5NtnCmwxMH8mFlRWl_ipie3O0aVzo` +scope | string | 否 | 该参数用于缩减 `user_access_token` 的权限范围。
例如:
1. 在[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时通过 `scope` 参数授权了 `contact:user.base:readonly contact:contact.base:readonly contact:user.employee:readonly` 三个权限。
2. 在当前接口可通过 `scope` 参数传入 `contact:user.base:readonly`,将 `user_access_token` 的权限缩减为 `contact:user.base:readonly` 这一个。
**注意**:
- 如果不指定当前参数,生成的 `user_access_token` 将包含用户授权时的所有权限。
- 当前参数不能传入重复的权限,否则会接口调用会报错,返回错误码 20067。
- 当前参数不能传入未授权的权限(即[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时用户已授权范围外的其他权限),否则接口调用会报错,返回错误码 20068。
- 多次调用当前接口缩减权限的范围不会叠加。例如,用户授予了权限 A 和 B,第一次调用该接口缩减为权限 A,则 `user_access_token` 只包含权限 A;第二次调用该接口缩减为权限 B,则 `user_access_token` 只包含权限 B。
- 生效的权限列表可通过本接口返回值 scope 查看。
**格式要求:** 以空格分隔的 `scope` 列表
**示例值:**`auth:user.id:read task:task:read` + +### 请求体示例 +```json +{ + "grant_type": "authorization_code", + "client_id": "cli_a5ca35a685b0x26e", + "client_secret": "baBqE5um9LbFGDy3X7LcfxQX1sqpXlwy", + "code": "a61hb967bd094dge949h79bbexd16dfe", + "redirect_uri": "https://example.com/api/oauth/callback", + "code_verifier": "TxYmzM4PHLBlqm5NtnCmwxMH8mFlRWl_ipie3O0aVzo" +} +``` +## 响应 +响应体类型为 `application/json; charset=utf-8`。 + +### 响应体 +**注意事项**:**响应体中的 `access_token` 和 `refresh_token` 长度较长**,一般在 1~2KB 之间,且可能由于 `scope` 数量的变多或后续变更导致长度进一步增加,建议预留 4KB 的存储容量 + +名称 | 类型 | 描述 +---|---|--- +code | int | 错误码,为 0 时表明请求成功,非 0 表示失败,请参照下文[错误码](#错误码)一节进行相应处理 +access_token | string | 即 `user_access_token`,仅在请求成功时返回 +expires_in | int | 即 `user_access_token` 的有效期,单位为秒,仅在请求成功时返回
**注意事项**:建议使用该字段以确定 `user_access_token` 的过期时间,不要硬编码有效期 +refresh_token | string | 用于刷新 `user_access_token`,详见[刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token)。该字段仅在请求成功且用户授予 `offline_access` 权限时返回。
**注意事项**:如果你在获取 `user_access_token` 时设置了 `scope` 请求参数,且需要返回 `refresh_token`,则需要在 `scope` 参数中包括 `offline_access`。另外,`refresh_token` 仅能被使用一次。 +refresh_token_expires_in | int | 即 `refresh_token` 的有效期,单位为秒,仅在返回 `refresh_token` 时返回。
**注意事项**:建议在到期前调用[刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token) 接口获取新的 `refresh_token`。 +token_type | string | 值固定为 `Bearer`,仅在请求成功时返回 +scope | string | 本次请求所获得的 `access_token` 所具备的权限列表,以空格分隔,仅在请求成功时返回 +error | string | 错误类型,仅在请求失败时返回 +error_description | string | 具体的错误信息,仅在请求失败时返回 + +### 响应体示例 + +成功响应示例: + +```json +{ + "code": 0, + "access_token": "eyJhbGciOiJFUzI1NiIs**********X6wrZHYKDxJkWwhdkrYg", + "expires_in": 7200, // 非固定值,请务必根据响应体中返回的实际值来确定 access_token 的有效期 + "refresh_token": "eyJhbGciOiJFUzI1NiIs**********XXOYOZz1mfgIYHwM8ZJA", + "refresh_token_expires_in": 604800, // 非固定值,请务必根据响应体中返回的实际值来确定 refresh_token 的有效期 + "scope": "auth:user.id:read offline_access task:task:read user_profile", + "token_type": "Bearer" +} +``` + +失败响应示例: +```json +{ + "code": 20050, + "error": "server_error", + "error_description": "An unexpected server error occurred. Please retry your request." +} +``` + +### 错误码 + +HTTP 状态码 | 错误码 | 描述 | 排查建议 +---|---|---|--- +400 | 20001 | The request is missing a required parameter. | 必要参数缺失,请检查请求时传入的参数是否有误 +400 | 20002 | The client secret is invalid. | 应用认证失败,请检查提供的 `client_id` 与 `client_secret` 是否正确 +400 | 20003 | The authorization code is not found. Please note that an authorization code can only be used once. | 无效的授权码,请检查授权码是否有效,注意授权码仅能使用一次 +400 | 20004 | The authorization code has expired. | 授权码已经过期,请在授权码生成后的 5 分钟内使用 +400 | 20008 | The user does not exist. | 用户不存在,请检查发起授权的用户的当前状态 +400 | 20009 | The specified app is not installed. | 租户未安装应用,请检查应用状态 +400 | 20010 | The user does not have permission to use this app. | 用户无应用使用权限,请检查发起授权的用户是否仍具有应用使用权限 +400 | 20024 | The provided authorization code or refresh token does not match the provided client ID. | 提供的授权码与 `client_id` 不匹配,请勿混用不同应用的凭证 +400 | 20036 | The specified grant_type is not supported. | 无效的 `grant_type`,请检查请求体中 `grant_type` 字段的取值 +400 | 20048 | The specified app does not exist. | 应用不存在,请检查应用状态 +400 | 20049 | PKCE code challenge failed. | PKCE 校验失败,请检查请求体中 `code_verifier` 字段是否存在且有效 +500 | 20050 | An unexpected server error occurred. Please retry your request. | 内部服务错误,请稍后重试,如果持续报错请联系[技术支持](https://applink.feishu.cn/TLJpeNdW) +400 | 20063 | The request is malformed. Please check your request. | 请求体中缺少必要字段,请根据具体的错误信息补齐字段 +400 | 20065 | The authorization code has been used. Please note that an authorization code can only be used once. | 授权码已被使用,授权码仅能使用一次,请检查是否有被重复使用 +400 | 20066 | The user status is invalid. | 用户状态非法,请检查发起授权的用户的当前状态 +400 | 20067 | The provided scope list contains duplicate scopes. Please ensure all scopes are unique. | 无效的 `scope` 列表,其中存在重复项,请确保传入的 `scope` 列表中没有重复项 +400 | 20068 | The provided scope list contains scopes that are not permitted. Please ensure all scopes are allowed. | 无效的 `scope` 列表,其中存在用户未授权的权限。当前接口 `scope` 参数传入的权限必须是[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时 `scope` 参数值的子集。
例如,在获取授权码时,用户授权了权限 A、B,则当前接口 `scope` 可传入的值只有权限 A、B,若传入权限 C 则会返回当前错误码。 +400 | 20069 | The specified app is not enabled. | 应用未启用,请检查应用状态 +400 | 20070 | Multiple authentication methods were provided. Please only use one to proceed. | 请求时同时使用了 `Basic Authentication` 和 `client_secret` 两种身份验证方式。请仅使用 `client_id`、`client_secret` 身份验证方式调用本接口。 +400 | 20071 | The provided redirect URI does not match the one used during authorization. | 无效的 `redirect_uri`,请确保 `redirect_uri` 与[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时传入的 `redirect_uri` 保持一致 +503 | 20072 | The server is temporarily unavailable. Please retry your request. | 服务暂不可用,请稍后重试 + +## 代码示例 +**注意事项**:此处提供的代码示例**仅供参考**,请勿直接在生产环境使用 + +### Golang + +运行下面示例程序的步骤: +1. 点击下方代码块右上角复制按钮,将代码复制到本地文件中,保存为 `main.go`; +2. 参照注释部分,完成配置; +3. 在 `main.go` 所在目录下新建 `.env` 文件,内容如下: + ```bash + APP_ID=cli_xxxxxx # 仅为示例值,请使用你的应用的 App ID,获取方式:开发者后台 -> 基础信息 -> 凭证与基础信息 -> 应用凭证 -> App ID + APP_SECRET=xxxxxx # 仅为示例值,请使用你的应用的 App Secret,获取方式:开发者后台 -> 基础信息 -> 凭证与基础信息 -> 应用凭证 -> App Secret + ``` +4. 在 `main.go` 所在目录执行以下命令: + ```bash + go mod init oauth-test + go get github.com/gin-gonic/gin + go get github.com/gin-contrib/sessions + go get github.com/gin-contrib/sessions/cookie + go get github.com/joho/godotenv + go get golang.org/x/oauth2 + go run main.go + ``` +5. 浏览器打开 [http://localhost:8080](http://localhost:8080) ,按照页面提示完成授权流程; + +```javascript +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "math/rand" + "net/http" + "os" + +"github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + _ "github.com/joho/godotenv/autoload" + "golang.org/x/oauth2" +) + +var oauthEndpoint = oauth2.Endpoint{ + AuthURL: "https://accounts.feishu.cn/open-apis/authen/v1/authorize", + TokenURL: "https://open.feishu.cn/open-apis/authen/v2/oauth/token", +} + +var oauthConfig = &oauth2.Config{ + ClientID: os.Getenv("APP_ID"), + ClientSecret: os.Getenv("APP_SECRET"), + RedirectURL: "http://localhost:8080/callback", // 请先添加该重定向 URL,配置路径:开发者后台 -> 开发配置 -> 安全设置 -> 重定向 URL -> 添加 + Endpoint: oauthEndpoint, + Scopes: []string{"offline_access"}, // 如果你不需要 refresh_token,请注释掉该行,否则你需要先申请 offline_access 权限方可使用,配置路径:开发者后台 -> 开发配置 -> 权限管理 +} + +func main() { + r := gin.Default() + +// 使用 Cookie 存储 session + store := cookie.NewStore([]byte("secret")) // 此处仅为示例,务必不要硬编码密钥 + r.Use(sessions.Sessions("mysession", store)) + +r.GET("/", indexController) + r.GET("/login", loginController) + r.GET("/callback", oauthCallbackController) + +fmt.Println("Server running on http://localhost:8080") + log.Fatal(r.Run(":8080")) +} + +func indexController(c *gin.Context) { + c.Header("Content-Type", "text/html; charset=utf-8") + var username string + session := sessions.Default(c) + if session.Get("user") != nil { + username = session.Get("user").(string) + } + html := fmt.Sprintf(`[返回主页](/) +`, user.Data.Name) + c.String(http.StatusOK, html) +} +``` + +# 刷新 user_access_token +OAuth 令牌接口,可用于刷新 user_access_token 以及获取新的 refresh_token。 + +- `user_access_token` 为用户访问凭证,使用该凭证可以以用户身份调用 OpenAPI,该凭证存在有效期,可通过 `refresh_token` 进行刷新。 +- 用户授权时,用户必须拥有[应用的使用权限](https://open.feishu.cn/document/home/introduction-to-scope-and-authorization/availability),否则调用本接口将会报错误码 20010。 +- `refresh_token` 用于获取新的 `user_access_token`,且仅能使用一次。在获取新的 `user_access_token` 时会返回新的 `refresh_token`,原 `refresh_token` 立即失效。 + +- 首次获取 `refresh_token` 的方式参见[获取 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)。 +**注意事项**:本接口实现遵循 [RFC 6749 - The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749) ,你可以使用[标准的 OAuth 客户端库](https://oauth.net/code/)进行接入(**推荐**) + +## 前置工作 +### 开通 offline_access 权限 +获取 `refresh_token` 需前往开放平台应用后台的**权限管理**模块开通 `offline_access` 权限,并在[发起授权](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时在 `scope` 参数中声明该权限。 + +![开通 offline_access 权限.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/f8b75edae2c682ab98b6984170707a64_gYt5eNq84a.png?height=703&lazyload=true&width=1867) + +在开通 `offline_access` 权限后,如需获取 `refresh_token`,具体的请求参数设置如下: +1. 首先在[发起授权](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时,授权链接的`scope` 参数中必须拼接 `offline_access`,例如: +``` +https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=cli_a5d611352af9d00b&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Foauth%2Fcallback&scope=bitable:app:readonly%20offline_access +``` +2. 在[获取 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)时, + + 如果不需要缩减权限,即该接口的 `scope` 参数为空,则无需做其他操作,即可正常获得 `refresh_token`; + + 如果需要缩减权限,即该接口的 `scope` 参数不为空, + + 且需要获取 `refresh_token`,则此处的 `scope` 参数中需要拼接 `offline_access`; + + 如不需要获取 `refresh_token`,则无需特殊处理; +3. 在[刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token)时,同第二步的逻辑。 + +### 开启刷新 user_access_token 的安全设置 +**注意事项**:- 如果你看不到此开关则无需关注,其默认处于开启状态。 +- 完成配置后需要发布应用使配置生效。具体操作参见[发布企业自建应用](https://open.feishu.cn/document/home/introduction-to-custom-app-development/self-built-application-development-process#baf09c7d)、[发布商店应用](https://open.feishu.cn/document/uMzNwEjLzcDMx4yM3ATM/uYjMyUjL2IjM14iNyITN)。 + +前往开放平台应用后台的**安全设置**模块,打开刷新 `user_access_token` 的开关。 + +![开启刷新 user_access_token 的安全设置.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/194824525c33e70cd796579744571c2d_WbBjYQW3zR.png?height=796&lazyload=true&width=1907) + +## 请求 +**注意事项**:为了避免刷新 `user_access_token` 的行为被滥用,在用户授权应用 365 天后,应用必须通过用户[重新授权](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)的方式来获取 `user_access_token` 与 `refresh_token`。如果 `refresh_token` 到期后继续刷新`user_access_token`将报错(错误码为 20037),可参考以下[错误码描述信息](#错误码)进行处理。 +**注意事项**:刷新后请更新本地 `user_access_token` 和 `refresh_token`,原令牌将无法再使用(`user_access_token` 会有一分钟的豁免时间以供应用完成令牌轮转)。 + +基本 |   +---|--- +HTTP URL | https://open.feishu.cn/open-apis/authen/v2/oauth/token +HTTP Method | POST +接口频率限制 | [1000 次/分钟、50 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN) +支持的应用类型 | Custom App、Store App +权限要求
**调用该 API 所需的权限。开启其中任意一项权限即可调用
** | 无 +字段权限要求 | `refresh_token` 以及 `refresh_token_expires_in` 字段仅在具备以下权限时返回:
offline_access(offline_access) + +### 请求头 + +名称 | 类型 | 必填 | 描述 +---|---|---|--- +Content-Type | string | 是 | 请求体类型。
**固定值:**`application/json; charset=utf-8` + +### 请求体 + +名称 | 类型 | 必填 | 描述 +---|---|---|--- +grant_type | string | 是 | 授权类型。
**固定值:**`refresh_token` +client_id | string | 是 | 应用的 App ID,可以在开发者后台中的应用详情页面找到该值。
**示例值:**`cli_a5ca35a685b0x26e` +client_secret | string | 是 | 应用的 App Secret,可以在开发者后台中的应用详情页面找到该值,详见:[如何获取应用的 App ID](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-app-id)。
**示例值:**`baBqE5um9LbFGDy3X7LcfxQX1sqpXlwy` +refresh_token | string | 是 | 刷新令牌,用于刷新 `user_access_token` 以及 `refresh_token`。
**注意事项**:请务必注意本接口仅支持[获取 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)和[刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token)接口返回的 `refresh_token`
**示例值:**`eyJhbGciOiJFUzI1NiIs**********XXOYOZz1mfgIYHwM8ZJA` +scope | string | 否 | 该参数用于缩减 `user_access_token` 的权限范围。
例如:
1. 在[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时通过 `scope` 参数授权了 `contact:user.base:readonly contact:contact.base:readonly contact:user.employee:readonly` 三个权限。
2. 在当前接口可通过 `scope` 参数传入 `contact:user.base:readonly`,将 `user_access_token` 的权限缩减为 `contact:user.base:readonly` 这一个。
**注意**:
- 如果不指定当前参数,生成的 `user_access_token` 将包含用户授权时的所有权限。
- 当前参数不能传入重复的权限,否则会接口调用会报错,返回错误码 20067。
- 当前参数不能传入未授权的权限(即[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时用户已授权范围外的其他权限),否则接口调用会报错,返回错误码 20068。
- 多次调用当前接口缩减权限的范围不会叠加。例如,用户授予了权限 A 和 B,第一次调用该接口缩减为权限 A,则 `user_access_token` 只包含权限 A;第二次调用该接口缩减为权限 B,则 `user_access_token` 只包含权限 B。
- 生效的权限列表可通过本接口返回值 scope 查看。
**格式要求:** 以空格分隔的 `scope` 列表
**示例值:**`auth:user.id:read task:task:read` + +### 请求体示例 +```json +{ + "grant_type": "refresh_token", + "client_id": "cli_a5ca35a685b0x26e", + "client_secret": "baBqE5um9LbFGDy3X7LcfxQX1sqpXlwy", + "refresh_token": "eyJhbGciOiJFUzI1NiIs**********XXOYOZz1mfgIYHwM8ZJA" +} +``` +## 响应 +响应体类型为 `application/json; charset=utf-8`。 + +### 响应体 + +名称 | 类型 | 描述 +---|---|--- +code | int | 错误码,为 0 时表明请求成功,非 0 表示失败,请参照下文错误码一节妥善处理 +access_token | string | 即 `user_access_token`,仅在请求成功时返回 +expires_in | int | 即 `user_access_token` 的有效期,单位为秒,仅在请求成功时返回
**注意事项**:建议使用该字段以确定 `user_access_token` 的过期时间,不要硬编码有效期 +refresh_token | string | 用于刷新 `user_access_token`,该字段仅在请求成功且用户授予 `offline_access` 权限时返回:
offline_access(offline_access)
**注意事项**:如果你在获取 `user_access_token` 时设置了 `scope` 请求参数,且需要返回 `refresh_token`,则需要在 `scope` 参数中包括 `offline_access`。另外,`refresh_token` 仅能被使用一次。 +refresh_token_expires_in | int | 即 `refresh_token` 的有效期,单位为秒,仅在返回 `refresh_token` 时返回。
**注意事项**:建议在到期前重新调用当前接口获取新的 `refresh_token`。 +token_type | string | 值固定为 `Bearer`,仅在请求成功时返回 +scope | string | 本次请求所获得的 `access_token` 所具备的权限列表,以空格分隔,仅在请求成功时返回 +error | string | 错误类型,仅在请求失败时返回 +error_description | string | 具体的错误信息,仅在请求失败时返回 + +### 响应体示例 + +成功响应示例: + +```json +{ + "code": 0, + "access_token": "eyJhbGciOiJFUzI1NiIs**********X6wrZHYKDxJkWwhdkrYg", + "expires_in": 7200, // 非固定值,请务必根据响应体中返回的实际值来确定 access_token 的有效期 + "refresh_token": "eyJhbGciOiJFUzI1NiIs**********VXOYOZYZmfgIYHWM0ZJA", + "refresh_token_expires_in": 604800, // 非固定值,请务必根据响应体中返回的实际值来确定 refresh_token 的有效期 + "scope": "auth:user.id:read offline_access task:task:read user_profile", + "token_type": "Bearer" +} +``` + +失败响应示例: +```json +{ + "code": 20050, + "error": "server_error", + "error_description": "An unexpected server error occurred. Please retry your request." +} +``` + +### 错误码 + +HTTP 状态码 | 错误码 | 描述 | 排查建议 +---|---|---|--- +400 | 20001 | The request is missing a required parameter. | 必要参数缺失,请检查请求时传入的参数是否有误 +400 | 20002 | The client secret is invalid. | 应用认证失败,请检查提供的 `client_id` 与 `client_secret` 是否正确。获取方式参见 [如何获取应用的 App ID](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-app-id)。 +400 | 20008 | The user does not exist. | 用户不存在,请检查发起授权的用户的当前状态 +400 | 20009 | The specified app is not installed. | 租户未安装应用,请检查应用状态 +400 | 20010 | The user does not have permission to use this app. | 用户无应用使用权限,请检查发起授权的用户是否仍具有应用使用权限 +400 | 20024 | The provided authorization code or refresh token does not match the provided client ID. | 提供的 `refresh_token` 与 `client_id` 不匹配,请勿混用不同应用的凭证 +400 | 20026 | The refresh token passed is invalid. Please check the value. | 请检查请求体中 `refresh_token` 字段的取值
请注意本接口仅支持 v2 版本接口下发的 `refresh_token` +400 | 20036 | The specified grant_type is not supported. | 无效的 `grant_type`,请检查请求体中 `grant_type` 字段的取值 +400 | 20037 | The refresh token passed has expired. Please generate a new one. | `refresh_token` 已过期,请[重新发起授权流程](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)以获取新的 `refresh_token` +400 | 20048 | The specified app does not exist. | 应用不存在,请检查应用状态 +500 | 20050 | An unexpected server error occurred. Please retry your request. | 内部服务错误,请稍后重试,如果持续报错请联系[技术支持](https://applink.feishu.cn/TLJpeNdW) +400 | 20063 | The request is malformed. Please check your request. | 请求体中缺少必要字段,请根据具体的错误信息补齐字段 +400 | 20064 | The refresh token has been revoked. Please note that a refresh token can only be used once. | `refresh_token` 已被撤销,请[重新发起授权流程](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)以获取新的 `refresh_token` +400 | 20066 | The user status is invalid. | 用户状态非法,请检查发起授权的用户的当前状态 +400 | 20067 | The provided scope list contains duplicate scopes. Please ensure all scopes are unique. | 无效的 `scope` 列表,其中存在重复项,请确保传入的 `scope` 列表中没有重复项 +400 | 20068 | The provided scope list contains scopes that are not permitted. Please ensure all scopes are allowed. | 无效的 `scope` 列表,其中存在用户未授权的权限。当前接口 `scope` 参数传入的权限必须是[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时 `scope` 参数值的子集。
例如,在获取授权码时,用户授权了权限 A、B,则当前接口 `scope` 可传入的值只有权限 A、B,若传入权限 C 则会返回当前错误码。 +400 | 20069 | The specified app is not enabled. | 应用未启用,请检查应用状态 +400 | 20070 | Multiple authentication methods were provided. Please only use one to proceed. | 请求时同时使用了 `Basic Authentication` 和 `client_secret` 两种身份验证方式。请仅使用 `client_id`、`client_secret` 身份验证方式调用本接口。 +503 | 20072 | The server is temporarily unavailable. Please retry your request. | 服务暂不可用,请稍后重试 +400 | 20073 | The refresh token has been used. Please note that a refresh token can only be used once. | 请[重新发起授权流程](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)以获取新的 `refresh_token` +400 | 20074 | The specified app is not allowed to refresh token. | 请在应用管理后台检查是否开启了刷新 `user_access_token` 开关,注意发版后生效 + diff --git a/public/dashboard-app.js b/public/dashboard-app.js index 78e7a3c..7184373 100644 --- a/public/dashboard-app.js +++ b/public/dashboard-app.js @@ -24,14 +24,39 @@ function switchTab(tab) { //如果切换到日志页,开始实时加载 if (tab === 'logs') { - startLogStreaming(); + checkAdminStatus().then(allowed => { + if (allowed) { + startLogStreaming(); + } else { + const container = document.getElementById('content-logs'); + container.innerHTML = ` +
+ +
+ `; + } + }); + return; } else { stopLogStreaming(); } //如果切换到设置页,加载 .env 文件 if (tab === 'settings') { - loadEnvFile(); + checkAdminStatus().then(allowed => { + if (allowed) { + loadEnvFile(); + } else { + const container = document.getElementById('content-settings'); + // 使用 iframe 嵌入错误页,保持侧边栏导航 + container.innerHTML = ` +
+ +
+ `; + } + }); + return; // 等待检查结果,暂不加载内容 } //如果切换到使用指南页,加载 README @@ -40,6 +65,17 @@ function switchTab(tab) { } } +async function checkAdminStatus() { + try { + const res = await fetch('/api/me'); + const data = await res.json(); + return data.loggedIn && data.isAdmin; + } catch (e) { + console.error('Check admin status failed:', e); + return false; + } +} + //日志流控制 let logInterval = null; let lastLogSize = 0; @@ -101,6 +137,17 @@ function escapeHtml(text) { async function loadDashboardData() { try { const res = await fetch('/api/status'); + + if (res.status === 403) { + document.getElementById('today-syncs').textContent = '无权限'; + document.getElementById('repo-count').textContent = '无权限'; + document.getElementById('error-count').textContent = '无权限'; + document.getElementById('uptime').textContent = '系统运行时间: 无权限'; + updateServiceStatus('unknown'); + loadHistory(); // 也会处理 403 + return; + } + const data = await res.json(); if (data.success) { @@ -124,6 +171,13 @@ async function loadDashboardData() { async function loadHistory() { try { const res = await fetch('/api/history'); + + if (res.status === 403) { + const tbody = document.getElementById('history-table'); + tbody.innerHTML = '无权限查看历史记录'; + return; + } + const data = await res.json(); if (data.success && data.history) { @@ -163,6 +217,15 @@ function updateServiceStatus(status) { 运行中 `; statusText.textContent = '运行中'; + } else if (status === 'unknown') { + badge.className = 'px-3 py-1 rounded-full text-xs font-medium flex items-center border bg-slate-100 text-slate-600 border-slate-200'; + badge.innerHTML = ` + + + + 状态未知 + `; + statusText.textContent = '状态未知'; } else { badge.className = 'px-3 py-1 rounded-full text-xs font-medium flex items-center border bg-slate-100 text-slate-600 border-slate-200'; badge.innerHTML = ` diff --git a/public/dashboard.html b/public/dashboard.html index db0ec6a..165a0fd 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -107,10 +107,14 @@ -
+
+ + + -
+