Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,28 @@
"@koa/router": "^15.1.1",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.1.0",
"@next/mdx": "^16.1.1",
"core-js": "^3.47.0",
"echarts-jsx": "^0.6.0",
"file-type": "^21.1.1",
"file-type": "^21.2.0",
"idea-react": "^2.0.0-rc.13",
"jsonwebtoken": "^9.0.3",
"koa": "^3.1.1",
"koa-jwt": "^4.0.4",
"koajax": "^3.1.2",
"license-filter": "^0.2.5",
"marked": "^17.0.1",
"mime": "^4.1.0",
"mobx": "^6.15.0",
"mobx-github": "^0.6.2",
"mobx-i18n": "^0.7.2",
"mobx-lark": "^2.5.0",
"mobx-lark": "^2.6.0",
"mobx-react": "^9.2.1",
"mobx-react-helper": "^0.5.1",
"mobx-restful": "^2.1.4",
"mobx-restful-table": "^2.6.3",
"mobx-strapi": "^0.8.1",
"next": "^16.1.0",
"next": "^16.1.1",
"next-pwa": "^5.6.0",
"next-ssr-middleware": "^1.1.0",
"open-react-map": "^0.9.1",
Expand All @@ -56,18 +58,19 @@
"@babel/preset-react": "^7.28.5",
"@cspell/eslint-plugin": "^9.4.0",
"@eslint/js": "^9.39.2",
"@next/eslint-plugin-next": "^16.1.0",
"@next/eslint-plugin-next": "^16.1.1",
"@open-source-bazaar/china-ngo-database": "^0.6.0",
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
"@stylistic/eslint-plugin": "^5.6.1",
"@types/eslint-config-prettier": "^6.11.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/koa": "^3.0.1",
"@types/next-pwa": "^5.6.9",
"@types/node": "^22.19.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.0",
"eslint-config-next": "^16.1.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-import-sort": "^12.1.1",
Expand All @@ -82,7 +85,7 @@
"prettier-plugin-css-order": "^2.1.2",
"sass": "^1.97.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.50.0"
"typescript-eslint": "^8.50.1"
},
"resolutions": {
"mobx-react-helper": "$mobx-react-helper",
Expand Down
18 changes: 18 additions & 0 deletions pages/api/Lark/document/copy/[...slug].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Context } from 'koa';
import { createKoaRouter, withKoaRouter } from 'next-ssr-middleware';

import { safeAPI, verifyJWT } from '../../../core';
import { lark } from '../../core';

export const config = { api: { bodyParser: false } };

const router = createKoaRouter(import.meta.url);

router.post('/:type/:id', safeAPI, verifyJWT, async (context: Context) => {
const { type, id } = context.params,
{ name, parentToken } = Reflect.get(context.request, 'body');

context.body = await lark.copyFile(`${type as 'wiki'}/${id}`, name, parentToken);
});
Comment on lines +11 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

类型安全问题:建议改进请求体访问方式

使用 Reflect.get(context.request, 'body') 绕过类型检查不是最佳实践。另外,type as 'wiki' 的强制类型断言可能导致运行时错误。

🔎 建议:添加类型定义和输入校验
+interface CopyRequestBody {
+  name: string;
+  parentToken: string;
+}
+
+type DocumentType = 'wiki' | 'docx';
+
 router.post('/:type/:id', safeAPI, verifyJWT, async (context: Context) => {
-  const { type, id } = context.params,
-    { name, parentToken } = Reflect.get(context.request, 'body');
+  const { type, id } = context.params;
+  const body = (context.request as any).body as CopyRequestBody;
+
+  if (!body?.name || !body?.parentToken) {
+    context.status = 400;
+    context.body = { message: 'Missing required fields: name, parentToken' };
+    return;
+  }
+
+  const validTypes: DocumentType[] = ['wiki', 'docx'];
+  if (!validTypes.includes(type as DocumentType)) {
+    context.status = 400;
+    context.body = { message: `Invalid document type: ${type}` };
+    return;
+  }

-  context.body = await lark.copyFile(`${type as 'wiki'}/${id}`, name, parentToken);
+  context.body = await lark.copyFile(`${type}/${id}`, body.name, body.parentToken);
 });
🤖 Prompt for AI Agents
In pages/api/Lark/document/copy/[...slug].ts around lines 11 to 16, the handler
uses Reflect.get(context.request, 'body') and a forced assertion type as 'wiki',
which bypasses type safety and can cause runtime errors; replace this with a
strongly typed request body interface (e.g., interface CopyRequest { name:
string; parentToken?: string }), parse and validate the body using the
framework's request body API into that type, validate required fields (name) and
that context.params.type is one of the allowed values (e.g., 'wiki') before
calling lark.copyFile, and return a 4xx error if validation fails instead of
using a blind type assertion.


export default withKoaRouter(router);
17 changes: 17 additions & 0 deletions pages/api/Lark/document/markdown/[...slug].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createKoaRouter, withKoaRouter } from 'next-ssr-middleware';

import { safeAPI, verifyJWT } from '../../../core';
import { lark } from '../../core';

const router = createKoaRouter(import.meta.url);

router.get('/:type/:id', safeAPI, verifyJWT, async context => {
const { type, id } = context.params;

const markdown = await lark.downloadMarkdown(`${type}/${id}`);

context.set('Content-Type', 'text/markdown; charset=utf-8');
context.body = markdown;
});

export default withKoaRouter(router);
12 changes: 7 additions & 5 deletions pages/api/Lark/file/[id]/[name].ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ const downloader: Middleware = async context => {
if (!ok) {
context.status = status;

return (context.body = await response.json());
try {
return (context.body = await response.json());
} catch {
return (context.body = await response.text());
}
}

const mime = headers.get('Content-Type'),
[stream1, stream2] = body!.tee();

Expand All @@ -44,9 +47,8 @@ const downloader: Middleware = async context => {
context.set('Content-Disposition', headers.get('Content-Disposition') || '');
context.set('Content-Length', headers.get('Content-Length') || '');

if (method === 'GET')
// @ts-expect-error Web type compatibility
context.body = Readable.fromWeb(stream2);
// @ts-expect-error Web type compatibility
context.body = method === 'GET' ? Readable.fromWeb(stream2) : '';
};

router.head('/:id/:name', safeAPI, downloader).get('/:id/:name', safeAPI, downloader);
Expand Down
56 changes: 36 additions & 20 deletions pages/api/core.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
import 'core-js/full/array/from-async';

import { Context, Middleware } from 'koa';
import { JsonWebTokenError, sign } from 'jsonwebtoken';
import { Context, Middleware, ParameterizedContext } from 'koa';
import JWT from 'koa-jwt';
import { HTTPError } from 'koajax';
import { Content } from 'mobx-github';
import { DataObject } from 'mobx-restful';
import { KoaOption, withKoa } from 'next-ssr-middleware';
import Path from 'path';
import { ProxyAgent, setGlobalDispatcher } from 'undici';
import YAML from 'yaml';
import { parse } from 'yaml';

import { LarkAppMeta } from '../../models/configuration';

const { HTTP_PROXY } = process.env;

if (HTTP_PROXY) setGlobalDispatcher(new ProxyAgent(HTTP_PROXY));

export type JWTContext = ParameterizedContext<
{ jwtOriginalError: JsonWebTokenError } | { user: DataObject }
>;

export const parseJWT = JWT({
secret: LarkAppMeta.secret,
cookie: 'token',
passthrough: true,
});

export const verifyJWT = JWT({ secret: LarkAppMeta.secret, cookie: 'token' });

const RobotToken = sign({ id: 0, name: 'Robot' }, LarkAppMeta.secret);

console.table({ RobotToken });

export const safeAPI: Middleware<any, any> = async (context: Context, next) => {
try {
return await next();
Expand Down Expand Up @@ -64,7 +83,7 @@ export function splitFrontMatter(raw: string) {
if (!frontMatter) return { markdown: raw };

try {
const meta = YAML.parse(frontMatter) as DataObject;
const meta = parse(frontMatter) as DataObject;

return { markdown, meta };
} catch (error) {
Expand All @@ -80,34 +99,31 @@ export async function* pageListOf(path: string, prefix = 'pages'): AsyncGenerato
const list = await readdir(prefix + path, { withFileTypes: true });

for (const node of list) {
let { name, path } = node;
let { name, parentPath } = node;

if (name.startsWith('.')) continue;

const isMDX = MDX_pattern.test(name);

({ name } = Path.parse(name));
path = `${path}/${name}`.replace(new RegExp(`^${prefix}`), '');
name = name.replace(MDX_pattern, '');
const path = `${parentPath}/${name}`.replace(new RegExp(`^${prefix}`), '');

if (node.isFile()) {
if (node.isFile() && isMDX) {
const article: ArticleMeta = { name, path, subs: [] };

if (isMDX)
try {
const rawFile = await readFile(`${node.path}/${node.name}`, { encoding: 'utf-8' });
const file = await readFile(`${parentPath}/${node.name}`, 'utf-8');

const { meta } = splitFrontMatter(rawFile);
const { meta } = splitFrontMatter(file);

if (meta) article.meta = meta;
} catch (error) {
console.error(`Error reading front matter for ${node.path}/${node.name}:`, error);
}
yield article;
} else if (node.isDirectory()) {
const subs = await Array.fromAsync(pageListOf(path, prefix));
if (meta) article.meta = meta;

if (subs[0]) yield { name, subs };
yield article;
}
if (!node.isDirectory()) continue;

const subs = await Array.fromAsync(pageListOf(path, prefix));

if (subs[0]) yield { name, subs };
}
}

Expand Down
Loading