- dissolve core-bridge into api (wire contract), domain (models), services (what the UI calls) and worker (everything that runs in the Web Worker, grouped by account, crypto, space, files, shares, runtime); regroup features, lib and store by responsibility and delete unused components - the UI only reads backend capabilities (domain/capabilities); one tokenRole picks the token, hasCode/isRetryable handle errors; pre-checks that decided permissions on the client are gone; tests fail if those rules are reimplemented - files go straight to the file domain (create, upload, download, delete, commit, cancel); the host sees one registration per file; creating a channel allocates a file domain and works even when the file service is down - file panel shows which channel the files belong to and its space usage; unlimited files preview in chat; videos get a stored cover; the file service status is shown - fix: console flooded by Retry-After reads on every upload frame
38 lines
2.6 KiB
TypeScript
38 lines
2.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
/**
|
|
* 前端不做权限和状态判断,只读后端给的能力、按后端的错误码处理。规则只在后端一处,这里守住前端不要重新实现:
|
|
* - 角色/权限:界面只通过 domain/capabilities 读后端给的 can_* 字段,不比较角色、不写 isOwner 判断;
|
|
* - 选哪张令牌:只有 tokenRole 一处;
|
|
* - 服务端为什么拒绝:只通过 hasCode(错误码),不比较 HTTP 状态码数字。
|
|
* 允许的例外写在 allowed 里,并说明理由。
|
|
*/
|
|
const modules = import.meta.glob<string>('/src/**/*.{ts,tsx}', { query: '?raw', import: 'default', eager: true });
|
|
const files = Object.entries(modules)
|
|
.map(([path, text]) => ({ path: path.replace(/^\/src\//, ''), text }))
|
|
.filter(({ path }) => !/\.test\.(ts|tsx)$/.test(path) && !path.startsWith('api/generated'));
|
|
|
|
interface Rule { name: string; pattern: RegExp; allowed: string[] }
|
|
const RULES: Rule[] = [
|
|
{ name: '不用 session.can(...) 自己判断权限(选令牌的 tokenRole、必须持有所有者密钥才能做的密码学操作、分享的所有权释放除外)', pattern: /session\.can\('(owner|write)'\)/,
|
|
allowed: ['worker/runtime/sessions.ts', 'worker/crypto/channelDataKeys.ts', 'worker/shares/shareState.ts', 'worker/shares/shareSeal.ts', 'worker/space/channelResources.ts', 'worker/space/channelTransport.ts'] },
|
|
{ name: '不比较 HTTP 状态码数字(请用 hasCode / isRetryable)', pattern: /\.status\s*(===|!==|>=|<=|<|>)\s*\d{3}\b/, allowed: ['worker/runtime/http.ts'] },
|
|
{ name: '不比较角色字符串(成员行的展示、授权包的密码学校验除外;分享的 mine 待后端提供)', pattern: /\.role\s*(===|!==)\s*'(owner|member|write|read)'/,
|
|
allowed: ['features/channel/members/MembersPane.tsx', 'features/channel/members/RolesEditor.tsx', 'worker/account/accountChannels.ts', 'worker/crypto/channelGrant.ts', 'worker/runtime/wire.ts'] },
|
|
];
|
|
|
|
describe('前端不重新实现后端的规则', () => {
|
|
it('扫描到了源码', () => { expect(files.length).toBeGreaterThan(100); });
|
|
for (const rule of RULES) {
|
|
it(rule.name, () => {
|
|
const offenders = files
|
|
.filter(({ path }) => !rule.allowed.includes(path))
|
|
.flatMap(({ path, text }) => text.split('\n')
|
|
.map((line, index) => ({ path, line: index + 1, text: line.trim() }))
|
|
.filter(({ text: line }) => !line.startsWith('//') && !line.startsWith('*') && rule.pattern.test(line))
|
|
.map(({ path: p, line, text: source }) => `${p}:${line} ${source}`));
|
|
expect(offenders, `这些地方重新实现了后端的规则:\n${offenders.join('\n')}`).toEqual([]);
|
|
});
|
|
}
|
|
});
|