# M2 鉴权与角色：为什么先做最小登录闭环

`M1` 完成之后，仓库里已经有了 `pnpm workspace + Turborepo` 的基本骨架，但这时如果直接冲向简历内容管理，很容易把后端职责、权限边界和后台入口一起做乱。

所以 `M2` 的目标不是“把功能做多”，而是先回答三个更基础的问题：

1. 这个项目的唯一业务后端是谁？
2. 后台里最小的角色边界是什么？
3. 从登录到受保护页面，这条通路能不能先跑通一个小闭环？

这篇文章就围绕这三个问题，拆开讲清楚 `M2` 为什么先做鉴权与角色，为什么只做最小登录闭环，以及它是怎样为 `M3` 之后的内容管理、AI 和导出打地基的。

## 一、为什么不是先做简历 CRUD

很多人在开始全栈重构时，会本能地先建数据表、先写简历模块、先做后台表单。这样当然也能推进，但教程型项目有一个额外约束：读者必须看得见每一步在解决什么问题。

如果 `M2` 一上来就做简历 CRUD，会立刻遇到这些混乱点：

- `web` 和 `admin` 到底直接调谁的接口，还没有统一约束
- `admin` 和 `viewer` 的角色语义还没固定，后面写编辑、发布、AI 时很容易边做边猜
- 后台前端虽然能先做出页面，但没有登录闭环时，很难解释哪些页面应该受保护、哪些动作该限制
- 后续再补权限时，往往要回头重构接口、守卫和前端体验，教程节奏会变得很碎

所以 `M2` 的任务不是扩业务，而是先把“入口”和“边界”收住。

更具体一点说，`M2` 要先完成下面这件事：

- 确认 `apps/server` 是唯一业务后端
- 确认项目当前只有 `admin` 和 `viewer` 两种最小角色
- 用 demo 账号和 JWT 跑通登录闭环
- 给 `apps/admin` 搭出一个最小登录壳
- 让 `viewer` 的只读限制真正落到接口和页面上

这几件事一旦成立，后面不管是简历发布、AI 分析还是导出下载，都是在明确边界上继续长，而不是边写边补规则。

## 二、M2 到底要交付什么

`M2` 的交付目标可以概括成一句话：

让读者能在本地看到“登录 -> 获得 token -> 进入后台 -> 根据角色体验不同动作”的最小闭环。

这里面有几个刻意的“只做一点点”。

### 1. 只做唯一业务后端，不急着接数据库

这一轮先建立 `apps/server`，让所有业务 API 都有明确归属。数据库、Redis、队列都先不接，因为本轮真正要验证的是后端边界，而不是持久化能力。

### 2. 只做 `admin / viewer` 两个角色

这不是因为系统永远只会有两个角色，而是因为对于 v1 教程项目来说，这已经足够支撑最关键的权限对比：

- `admin`：可以写、可以触发敏感动作
- `viewer`：可以看，但不能改、不能发布、不能触发真实 AI

角色模型越早收敛，后面越不会散。

### 3. 只做最小 JWT 闭环，不急着上完整鉴权体系

这一轮没有刷新令牌，没有数据库用户表，也没有把 Passport 全家桶一口气接满。原因很简单：教程的重点是先让读者看懂认证链路，而不是一开始就陷入复杂鉴权栈的细节。

### 4. 只做后台登录壳，不急着做完整后台

`apps/admin` 在 `M2` 里只是一个壳：

- 有登录页
- 能调用 `apps/server`
- 能保存 token
- 登录后能进入最小受保护页面

这样做的好处是，读者能尽快在浏览器里看到“这条链路确实跑通了”，而不是停留在后端接口级别。

## 三、M2 是怎么一步步拆成 5 个 issue 的

这一轮的拆法很关键，因为它体现了“先固定边界，再跑通闭环”的顺序。

### issue-05：先把 `apps/server` 立起来

第一步不是写登录接口，而是先创建最小 `NestJS` 脚手架，把唯一业务后端的入口明确下来。

这里最重要的不是脚手架本身，而是仓库语义的变化：

- 后续所有业务能力都有了统一承载入口
- `web` 和 `admin` 的调用方向开始变得稳定
- “避免双后端”第一次从文档原则落到真实目录结构里

这一轮故意不接数据库，是为了让读者先看清“服务边界”。

### issue-06：先写角色模型，再写登录

如果不先固定角色语义，后面就会很容易出现这种情况：

- 后端 guard 写一套角色判断
- 前端页面再自己写一套“这个按钮要不要禁用”
- 文档里又写一套“viewer 应该只能看”

这三套一旦分开，后面必然漂移。

所以 `M2` 第二步是先收敛角色能力矩阵，用领域规则把 `admin / viewer` 的最小边界固化下来。这样后面不管是接口守卫、前端提示还是测试断言，都能围绕同一份语义展开。

### issue-07：再做最小 JWT 登录闭环

角色模型固定之后，才进入真正的登录链路。

这一轮选择 demo 账号而不是数据库用户表，有两个很实际的原因：

- 读者可以先聚焦“登录行为”本身
- 不会因为用户表、密码哈希、迁移脚本把教学重点冲散

最小闭环只做三件事：

- 登录接口
- 签发 token
- 受保护接口

只要这三步能跑通，后面所有后台功能都有了承载基础。

### issue-08：给 `apps/admin` 一个最小登录壳

后端闭环跑通以后，如果没有前端壳，读者的体验仍然是不完整的，因为看不到真实浏览器环境中的登录、跳转和会话效果。

所以这一轮才创建 `apps/admin`，而且只做最小壳层，不做业务页面：

- 登录页负责采集账号密码
- 页面直接调用 `NestJS` 登录接口
- 拿到 token 后保存到前端会话层
- 再请求 `/auth/me` 验证当前用户身份

这里也顺手把“为什么不要双后端”讲清楚了：`Next.js` 负责页面和壳，不负责再复制一套业务接口。

### issue-09：最后把 `viewer` 只读落到真实体验

如果只做到了“能登录”，那还不够。教程里真正有价值的一步，是让读者看见角色差异真的影响了系统行为。

所以 `issue-09` 让 `viewer` 的只读限制同时落到两层：

- 后端：通过能力守卫限制敏感动作
- 前端：通过禁用按钮和只读提示表达角色差异

这样一来，角色模型就不再只是一个枚举，而是变成了可以在接口、页面和测试里都被验证的真实边界。

## 四、这轮最重要的几个设计取舍

### 1. 坚持 `NestJS` 是唯一业务后端

这是整个项目后续能保持清晰的关键前提。

在这个项目里：

- `Next.js` 负责页面、路由壳、SSR 能力和前端会话承接
- `NestJS` 负责业务 API、权限、文件、导出、AI 与任务链路

这样做的好处不只是“架构更整洁”，更重要的是教程更容易讲清楚。否则读者很快会遇到这些问题：

- 为什么有的接口在 `Next Route Handlers`，有的接口在 `NestJS`
- DTO、鉴权、错误码应该看哪一套
- 后续接移动端或别的前端时，到底复用谁

所以 `M2` 看起来只是在做登录，但实际上是在给整个项目的后续 API 方向定规矩。

### 2. 先用 demo 账号，而不是立刻接数据库

这不是偷懒，而是教学节奏上的取舍。

如果 `M2` 一开始就接数据库，读者会同时面对这些信息：

- 用户表设计
- ORM 配置
- 迁移脚本
- 密码加密
- 登录接口
- 守卫与 token

信息量太大时，真正想讲的“最小登录闭环”反而会失焦。

demo 账号的价值在于，它能先把行为证明出来。等读者已经理解了：

- 登录请求长什么样
- token 是怎么发出去的
- 受保护接口如何恢复当前用户

再进入持久化和真实用户体系，就会清晰很多。

### 3. `viewer` 只读必须同时落在后端和前端

很多项目会只做其中一层：

- 只做后端 guard，前端什么都不提示
- 或者只做前端禁用，后端其实没拦住

这两种都不够。

如果只在后端限制，用户不知道为什么自己不能做；如果只在前端限制，接口边界其实并不可靠。

所以 `M2` 刻意把两层都补上：

- 后端真的拦
- 前端明确说清楚

这样读者能第一次看到“角色语义如何从领域规则走到真实体验”。

## 五、关键文件应该怎么看

如果你想顺着代码复盘 `M2`，可以先抓住下面这些文件：

- `apps/server/src/modules/auth/domain/auth-role-policy.ts`
- `apps/server/src/modules/auth/auth.service.ts`
- `apps/server/src/modules/auth/guards/jwt-auth.guard.ts`
- `apps/server/src/modules/auth/guards/role-capabilities.guard.ts`
- `apps/admin/components/admin-login-shell.tsx`
- `apps/admin/components/admin-dashboard-shell.tsx`
- `apps/admin/components/role-action-panel.tsx`

推荐的阅读顺序是：

1. 先看角色能力策略，理解 `admin / viewer` 的边界
2. 再看 `AuthService` 和 JWT guard，理解登录闭环
3. 最后看 `apps/admin` 的登录壳和只读体验，理解前端如何只做壳层承接

这样读起来，不容易把“业务边界”和“界面表现”混在一起。

## 六、M2 的测试是怎么设计的

这一轮虽然功能不多，但测试设计非常重要，因为它决定后面重构时边界会不会漂。

### 1. 先用领域测试锁住角色语义

角色能力测试不是在验证“代码有没有跑”，而是在验证一件更重要的事：`viewer` 和 `admin` 到底分别能做什么。

这一步先锁住之后，后面的守卫和前端提示才有稳定依据。

### 2. 再用单测和 E2E 锁住登录行为

登录这一块至少要证明：

- 正确账号可以拿到 token
- 错误账号会被拒绝
- 带 token 可以访问受保护接口
- 不带 token 会被拒绝

也就是说，M2 的重点不是“测试多不多”，而是把闭环上最容易漂移的行为锁住。

### 3. 最后做一轮真实浏览器联调

这一步很值得强调。

单测和 E2E 都通过，不代表浏览器里真的没问题。比如跨端请求的 CORS、token 存储、页面跳转这些，都只有在真实联调里才容易暴露。

因此 `M2` 最后一定要做一次真实浏览器验证，让读者知道：

- 不是只有接口能通
- 而是后台最小入口真的能用了

## 七、这一轮最容易踩的坑

### 1. 角色语义写在文档里，却没落到代码里

如果 `viewer` 只停留在说明文档中，后面一接内容发布、AI 和导出，很快就会出现各写各的权限判断。

正确做法是尽早把能力矩阵沉到领域规则和守卫里。

### 2. 后端闭环已经跑通，却迟迟没有前端壳

这会让整个 `M2` 看起来像“只有一堆接口”。对教程读者来说，缺少浏览器里的真实反馈，理解成本会高很多。

### 3. 为了“完整”而过度设计鉴权体系

一旦在这一步把刷新令牌、数据库用户表、复杂会话策略、完整 Passport 体系全部压进来，`M2` 就会从“最小闭环”变成“第一轮就做完一大半认证系统”。

这不利于教学，也不利于后面按里程碑写教程。

## 八、M2 完成之后，M3 为什么会自然很多

`M2` 最大的价值，不是只得到一个登录页，而是让后面的每个功能都能站在稳定边界上继续长。

到了 `M3`，我们终于可以比较安心地进入双语简历内容与发布流，因为这时几个前提已经成立了：

- 后端入口已经明确
- 后台前端已经有最小登录壳
- 角色边界已经固定
- `viewer` 只读语义已经能被验证

这意味着 `M3` 不需要一边做内容模块，一边再回头补“到底谁能发布、谁能编辑、后台怎么登录”。

换句话说，`M2` 不是在拖慢进度，而是在减少后续反复返工的概率。

## 九、如果你要自己复现 M2，建议按这个顺序来

1. 先建 `apps/server`，明确唯一业务后端
2. 先定义角色模型和能力矩阵
3. 再实现最小登录接口与受保护接口
4. 再搭 `apps/admin` 的最小登录壳
5. 最后把 `viewer` 只读真正落到接口和页面

这个顺序的好处是，每一步都能回答“我现在在解决什么问题”，不会一上来就同时面对太多系统层面的复杂度。

## 十、这篇文章对应的原始材料

如果你想继续往下深挖实现细节，可以配合下面这些开发日志一起阅读：

- `docs/30-开发日志/M2-issue-05-apps-server-最小-NestJS-脚手架.md`
- `docs/30-开发日志/M2-issue-06-鉴权领域与角色模型.md`
- `docs/30-开发日志/M2-issue-07-JWT-登录最小闭环.md`
- `docs/30-开发日志/M2-issue-08-apps-admin-最小登录壳.md`
- `docs/30-开发日志/M2-issue-09-viewer-只读守卫与体验.md`

这组日志更适合看“具体怎么做”，而本文更希望回答“为什么要这样拆”。

## 小结

`M2` 做的并不是一个完整后台，而是先把教程型项目最容易被忽略的地基补齐：

- 唯一业务后端
- 最小角色模型
- 最小 JWT 登录闭环
- 最小后台登录壳
- `viewer` 只读的真实落地

这些东西看起来都不大，但它们决定了后面 `M3` 到 `M10` 能不能沿着一条清晰主线继续长下去。
