/** * Shrimp basic-admin demo * * 用 MemoryShrimpCrud 跑一个零依赖的 admin server,展示: * - Shrimp 核心(模块注册 / RPC dispatch / 权限树) * - ShrimpTableModule + MemoryShrimpCrud 的 CRUD 自动事件 * - 通过 Express 暴露成完整的 hand-admin 协议: * GET /admin-api/config * POST /admin-api/login * GET /admin-api/me * POST /admin-api/[/...]/ * * 不使用 ShrimpServer/Mongoose/JWT,刻意暴露 ShrimpServer 把太多东西耦合在 * Express+Mongo+Auth 一起的事实,作为后续重构的对照样本。 * * 单一 mock super-admin operator,前端登录直接放行,token 是固定字符串。 */ import express, { type Request, type Response } from "express" import { Shrimp, ShrimpTableModule, MemoryShrimpCrud, ShrimpFieldTypes, ShrimpError, ShrimpCode, } from "shrimp-server" import type { IFieldConfig, WithId, IShrimpOperator, IModuleInfo, IMenuItem, IMeResult, } from "shrimp-server" interface ITag { _id: string; name: string } interface IProduct { _id: string; code: string; name: string; price: number; tagIds: string[] } const tagFields: IFieldConfig[] = [ { key: "name", label: "标签名", type: ShrimpFieldTypes.String, required: true, searchable: true }, ] const productFields: IFieldConfig[] = [ { key: "code", label: "编码", type: ShrimpFieldTypes.String, required: true, editable: "addOnly" }, { key: "name", label: "名称", type: ShrimpFieldTypes.String, required: true, searchable: true }, { key: "price", label: "价格", type: ShrimpFieldTypes.Number, required: true }, { key: "tagIds", label: "标签", type: ShrimpFieldTypes.Relation, relation: { moduleKey: "tag", labelKey: "name" }, multiple: true, defaultValue: [], }, ] const FAKE_TOKEN = "demo-super-admin-token" const superAdmin: IShrimpOperator = { userId: "demo-admin", roleId: ["demo-super"], isSuperAdmin: true, } async function bootstrap() { const shrimp = new Shrimp() const tagCrud = new MemoryShrimpCrud>() new ShrimpTableModule(shrimp, "tag", tagCrud, tagFields, "标签管理").initCrud() const productCrud = new MemoryShrimpCrud>() new ShrimpTableModule(shrimp, "product", productCrud, productFields, "商品管理").initCrud() // 种子数据 const t1 = await tagCrud.add({ name: "热门" } as any) const t2 = await tagCrud.add({ name: "促销" } as any) await productCrud.add({ code: "P001", name: "苹果", price: 5, tagIds: [t1._id] } as any) await productCrud.add({ code: "P002", name: "香蕉", price: 3, tagIds: [t1._id, t2._id] } as any) // super admin 看得到所有模块 const allKeys = (await shrimp.allPower()).map(p => p.key) const superPowers = new Set(allKeys) const app = express() app.use(express.json()) // 简单认证: 仅检查 Authorization,通过即注入 super-admin operator function authedRunner(req: Request, res: Response, run: () => Promise) { const auth = req.headers.authorization || "" const token = auth.replace(/^Bearer\s+/i, "") if (token !== FAKE_TOKEN) { res.json({ code: ShrimpCode.UNAUTHORIZED, tip: { t: "shrimp.relogin" } }) return } Shrimp.asyncLocalStorage.run(superAdmin, async () => { try { const data = await run() res.json({ code: ShrimpCode.OK, data }) } catch (e: any) { if (e instanceof ShrimpError) { res.json({ code: e.code, tip: e.tip }) } else { console.error(e) res.json({ code: ShrimpCode.SERVER_ERROR, log: String(e?.message || e) }) } } }) } // --- 公开接口 --- app.get("/admin-api/config", (_req, res) => { res.json({ code: ShrimpCode.OK, data: { title: "Shrimp Basic Admin Demo" } }) }) app.post("/admin-api/login", (req, res) => { const { username, password } = req.body || {} // demo 模式不校验密码,任意账号都给同一个 token if (!username || !password) { res.json({ code: ShrimpCode.UNAUTHORIZED, tip: { t: "shrimp.loginFailed" } }) return } res.json({ code: ShrimpCode.OK, data: { token: FAKE_TOKEN } }) }) // --- 需要登录的接口 --- app.get("/admin-api/me", (req, res) => { authedRunner(req, res, async () => { const moduleInfo: IModuleInfo[] = shrimp.getModuleInfo(superPowers) const fullMenu = shrimp.getMenuTree() const menuTree: IMenuItem[] = shrimp.filterMenuTree(fullMenu, allKeys) const result: IMeResult = { nickname: "Demo Admin", moduleInfo, menuTree, options: {}, } return result }) }) // RPC dispatch: POST /admin-api/<...path> body 是参数数组 app.post(/^\/admin-api\/.+/, (req, res) => { const rawPath = req.path.replace(/^\/admin-api\//, "") // 不要把 login/config 也接进来 if (rawPath === "login" || rawPath === "config" || rawPath === "me") { res.status(404).end() return } authedRunner(req, res, async () => { const path = rawPath.split("/").filter(Boolean) const args = Array.isArray(req.body) ? req.body : [] return shrimp.call(path, ...args) }) }) const port = 3100 app.listen(port, () => { console.log(`Shrimp basic-admin demo: http://localhost:${port}`) console.log("") console.log("# 配合前端: cd web && npm install && npm run dev,然后访问 http://localhost:5173") console.log("# 登录: 任意账号密码") console.log("") console.log("# 也可以直接 curl:") console.log(` TOKEN=${FAKE_TOKEN}`) console.log(` curl -s -X POST http://localhost:${port}/admin-api/product/list -H 'content-type: application/json' -H "Authorization: Bearer $TOKEN" -d '[{}]' | jq`) }) } bootstrap().catch(err => { console.error("bootstrap failed:", err) process.exit(1) })