博客 API 教程
本教程将指导您使用 MTPC 构建一个完整的博客 API,包括文章管理、评论系统和用户权限控制。
项目概述
我们将构建一个具有以下功能的博客 API:
- 用户注册和登录
- 文章 CRUD 操作
- 评论系统
- 角色和权限管理
- 多租户支持
技术栈
- MTPC: 权限和资源管理
- Hono: Web 框架
- Drizzle ORM: 数据库 ORM
- PostgreSQL: 数据库
- Zod: 数据验证
项目结构
blog-api/
├── src/
│ ├── config/
│ │ └── config.ts
│ ├── db/
│ │ ├── schema.ts
│ │ └── connection.ts
│ ├── resources/
│ │ ├── user.ts
│ │ ├── post.ts
│ │ └── comment.ts
│ ├── routes/
│ │ ├── auth.ts
│ │ ├── posts.ts
│ │ └── comments.ts
│ └── index.ts
├── package.json
└── tsconfig.json步骤 1: 项目初始化
安装依赖
pnpm init
pnpm add @mtpc/core @mtpc/rbac @mtpc/adapter-hono @mtpc/adapter-drizzle
pnpm add hono drizzle-orm postgres
pnpm add zod bcryptjs jsonwebtoken
pnpm add -D @types/bcryptjs @types/jsonwebtoken typescript配置 TypeScript
创建 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}步骤 2: 数据库配置
创建数据库连接
创建 src/db/connection.ts:
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
const connectionString = process.env.DATABASE_URL || 'postgresql://localhost/blog'
export const db = drizzle(postgres(connectionString))定义数据库 Schema
创建 src/db/schema.ts:
import { pgTable, serial, text, timestamp, uuid, integer } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
password: text('password').notNull(),
name: text('name').notNull(),
role: text('role').notNull().default('user'),
tenantId: uuid('tenant_id').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
})
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
title: text('title').notNull(),
content: text('content').notNull(),
authorId: uuid('author_id').notNull().references(() => users.id),
tenantId: uuid('tenant_id').notNull(),
status: text('status').notNull().default('draft'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
})
export const comments = pgTable('comments', {
id: uuid('id').primaryKey().defaultRandom(),
content: text('content').notNull(),
postId: uuid('post_id').notNull().references(() => posts.id),
authorId: uuid('author_id').notNull().references(() => users.id),
tenantId: uuid('tenant_id').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
})步骤 3: 定义资源
用户资源
创建 src/resources/user.ts:
import { defineResource } from '@mtpc/core'
import { z } from 'zod'
const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'author', 'reader']),
tenantId: z.string().uuid(),
})
export const userResource = defineResource({
name: 'user',
schema: userSchema,
features: {
creatable: true,
readable: true,
updatable: true,
deletable: false,
listable: true,
},
permissions: [
{ action: 'create', description: '创建用户', scope: 'tenant' },
{ action: 'read', description: '查看用户', scope: 'tenant' },
{ action: 'update', description: '更新用户', scope: 'own' },
{ action: 'list', description: '列出用户', scope: 'tenant' },
],
metadata: {
displayName: '用户',
pluralName: '用户列表',
group: 'user-management',
},
})文章资源
创建 src/resources/post.ts:
import { defineResource } from '@mtpc/core'
import { z } from 'zod'
const postSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1).max(200),
content: z.string().min(1),
authorId: z.string().uuid(),
tenantId: z.string().uuid(),
status: z.enum(['draft', 'published', 'archived']),
})
export const postResource = defineResource({
name: 'post',
schema: postSchema,
features: {
creatable: true,
readable: true,
updatable: true,
deletable: true,
listable: true,
},
permissions: [
{ action: 'create', description: '创建文章', scope: 'tenant' },
{ action: 'read', description: '查看文章', scope: 'tenant' },
{ action: 'update', description: '更新文章', scope: 'own' },
{ action: 'delete', description: '删除文章', scope: 'own' },
{ action: 'publish', description: '发布文章', scope: 'own' },
],
metadata: {
displayName: '文章',
pluralName: '文章列表',
group: 'content-management',
},
relations: [{
name: 'author',
type: 'belongsTo',
target: 'user',
foreignKey: 'authorId',
description: '文章作者',
}],
})评论资源
创建 src/resources/comment.ts:
import { defineResource } from '@mtpc/core'
import { z } from 'zod'
const commentSchema = z.object({
id: z.string().uuid(),
content: z.string().min(1).max(1000),
postId: z.string().uuid(),
authorId: z.string().uuid(),
tenantId: z.string().uuid(),
})
export const commentResource = defineResource({
name: 'comment',
schema: commentSchema,
features: {
creatable: true,
readable: true,
updatable: true,
deletable: true,
listable: true,
},
permissions: [
{ action: 'create', description: '创建评论', scope: 'tenant' },
{ action: 'read', description: '查看评论', scope: 'tenant' },
{ action: 'update', description: '更新评论', scope: 'own' },
{ action: 'delete', description: '删除评论', scope: 'own' },
],
metadata: {
displayName: '评论',
pluralName: '评论列表',
group: 'content-management',
},
relations: [{
name: 'post',
type: 'belongsTo',
target: 'post',
foreignKey: 'postId',
description: '所属文章',
}],
})步骤 4: 配置 MTPC
创建 src/config/config.ts:
import { createMTPC } from '@mtpc/core'
import { createRBACPlugin } from '@mtpc/rbac'
import { userResource, postResource, commentResource } from '../resources'
const mtpc = createMTPC({
registry: {
resources: [userResource, postResource, commentResource],
},
})
const rbacPlugin = createRBACPlugin()
mtpc.use(rbacPlugin)
export async function initMTPC() {
await mtpc.init()
// 创建默认角色
await rbacPlugin.createRole('admin', {
permissions: [
{ resource: 'user', action: 'create' },
{ resource: 'user', action: 'read' },
{ resource: 'user', action: 'update' },
{ resource: 'user', action: 'list' },
{ resource: 'post', action: 'create' },
{ resource: 'post', action: 'read' },
{ resource: 'post', action: 'update' },
{ resource: 'post', action: 'delete' },
{ resource: 'post', action: 'publish' },
{ resource: 'post', action: 'list' },
{ resource: 'comment', action: 'create' },
{ resource: 'comment', action: 'read' },
{ resource: 'comment', action: 'update' },
{ resource: 'comment', action: 'delete' },
{ resource: 'comment', action: 'list' },
],
})
await rbacPlugin.createRole('author', {
permissions: [
{ resource: 'user', action: 'read' },
{ resource: 'user', action: 'update', scope: 'own' },
{ resource: 'post', action: 'create' },
{ resource: 'post', action: 'read' },
{ resource: 'post', action: 'update', scope: 'own' },
{ resource: 'post', action: 'delete', scope: 'own' },
{ resource: 'post', action: 'publish', scope: 'own' },
{ resource: 'post', action: 'list' },
{ resource: 'comment', action: 'create' },
{ resource: 'comment', action: 'read' },
{ resource: 'comment', action: 'update', scope: 'own' },
{ resource: 'comment', action: 'delete', scope: 'own' },
],
})
await rbacPlugin.createRole('reader', {
permissions: [
{ resource: 'user', action: 'read', scope: 'own' },
{ resource: 'post', action: 'read' },
{ resource: 'post', action: 'list' },
{ resource: 'comment', action: 'create' },
{ resource: 'comment', action: 'read' },
{ resource: 'comment', action: 'update', scope: 'own' },
{ resource: 'comment', action: 'delete', scope: 'own' },
],
})
}
export { mtpc, rbacPlugin }步骤 5: 创建路由
认证路由
创建 src/routes/auth.ts:
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { db } from '../db/connection'
import { users } from '../db/schema'
import { eq } from 'drizzle-orm'
const authRouter = new Hono()
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
name: z.string().min(1),
tenantId: z.string().uuid(),
})
authRouter.post('/register', zValidator('json', registerSchema), async (c) => {
const { email, password, name, tenantId } = c.req.valid('json')
// 检查用户是否已存在
const existingUser = await db.select().from(users).where(eq(users.email, email))
if (existingUser.length > 0) {
return c.json({ error: 'User already exists' }, 400)
}
// 加密密码
const hashedPassword = await bcrypt.hash(password, 10)
// 创建用户
const [newUser] = await db.insert(users).values({
email,
password: hashedPassword,
name,
tenantId,
role: 'reader',
}).returning()
// 绑定默认角色
await rbacPlugin.bindRole(newUser.id, 'reader')
// 生成 JWT
const token = jwt.sign(
{ userId: newUser.id, tenantId: newUser.tenantId, roles: ['reader'] },
process.env.JWT_SECRET || 'secret',
{ expiresIn: '7d' }
)
return c.json({ token, user: { id: newUser.id, email: newUser.email, name: newUser.name } })
})
const loginSchema = z.object({
email: z.string().email(),
password: z.string(),
})
authRouter.post('/login', zValidator('json', loginSchema), async (c) => {
const { email, password } = c.req.valid('json')
// 查找用户
const [user] = await db.select().from(users).where(eq(users.email, email))
if (!user) {
return c.json({ error: 'Invalid credentials' }, 401)
}
// 验证密码
const isValidPassword = await bcrypt.compare(password, user.password)
if (!isValidPassword) {
return c.json({ error: 'Invalid credentials' }, 401)
}
// 获取用户角色
const roleBindings = await rbacPlugin.getRoleBindings(user.id)
const roles = roleBindings.map(b => b.roleId)
// 生成 JWT
const token = jwt.sign(
{ userId: user.id, tenantId: user.tenantId, roles },
process.env.JWT_SECRET || 'secret',
{ expiresIn: '7d' }
)
return c.json({ token, user: { id: user.id, email: user.email, name: user.name, roles } })
})
export { authRouter }文章路由
创建 src/routes/posts.ts:
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { db } from '../db/connection'
import { posts } from '../db/schema'
import { eq, and } from 'drizzle-orm'
import { createContext } from '@mtpc/core'
import { mtpc } from '../config/config'
const postsRouter = new Hono()
// 认证中间件
const authMiddleware = async (c: any, next: any) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'secret') as any
c.set('user', decoded)
await next()
} catch (error) {
return c.json({ error: 'Invalid token' }, 401)
}
}
postsRouter.use(authMiddleware)
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
status: z.enum(['draft', 'published', 'archived']).default('draft'),
})
postsRouter.post('/', zValidator('json', createPostSchema), async (c) => {
const user = c.get('user')
const data = c.req.valid('json')
// 检查权限
const ctx = createContext({
subject: { id: user.userId, roles: user.roles },
tenant: { id: user.tenantId },
})
const hasPermission = await mtpc.checkPermission(ctx, 'post', 'create')
if (!hasPermission.allowed) {
return c.json({ error: 'Permission denied' }, 403)
}
// 创建文章
const [newPost] = await db.insert(posts).values({
...data,
authorId: user.userId,
tenantId: user.tenantId,
}).returning()
return c.json(newPost)
})
postsRouter.get('/', async (c) => {
const user = c.get('user')
// 检查权限
const ctx = createContext({
subject: { id: user.userId, roles: user.roles },
tenant: { id: user.tenantId },
})
const hasPermission = await mtpc.checkPermission(ctx, 'post', 'list')
if (!hasPermission.allowed) {
return c.json({ error: 'Permission denied' }, 403)
}
// 获取文章列表
const allPosts = await db.select().from(posts).where(eq(posts.tenantId, user.tenantId))
return c.json(allPosts)
})
postsRouter.get('/:id', async (c) => {
const user = c.get('user')
const id = c.req.param('id')
// 检查权限
const ctx = createContext({
subject: { id: user.userId, roles: user.roles },
tenant: { id: user.tenantId },
})
const hasPermission = await mtpc.checkPermission(ctx, 'post', 'read')
if (!hasPermission.allowed) {
return c.json({ error: 'Permission denied' }, 403)
}
// 获取文章
const [post] = await db.select().from(posts).where(
and(eq(posts.id, id), eq(posts.tenantId, user.tenantId))
)
if (!post) {
return c.json({ error: 'Post not found' }, 404)
}
return c.json(post)
})
postsRouter.put('/:id', zValidator('json', createPostSchema.partial()), async (c) => {
const user = c.get('user')
const id = c.req.param('id')
const data = c.req.valid('json')
// 检查权限
const ctx = createContext({
subject: { id: user.userId, roles: user.roles },
tenant: { id: user.tenantId },
resource: { id, authorId: user.userId },
})
const hasPermission = await mtpc.checkPermission(ctx, 'post', 'update')
if (!hasPermission.allowed) {
return c.json({ error: 'Permission denied' }, 403)
}
// 更新文章
const [updatedPost] = await db.update(posts)
.set({ ...data, updatedAt: new Date() })
.where(and(eq(posts.id, id), eq(posts.tenantId, user.tenantId)))
.returning()
return c.json(updatedPost)
})
postsRouter.delete('/:id', async (c) => {
const user = c.get('user')
const id = c.req.param('id')
// 检查权限
const ctx = createContext({
subject: { id: user.userId, roles: user.roles },
tenant: { id: user.tenantId },
resource: { id, authorId: user.userId },
})
const hasPermission = await mtpc.checkPermission(ctx, 'post', 'delete')
if (!hasPermission.allowed) {
return c.json({ error: 'Permission denied' }, 403)
}
// 删除文章
await db.delete(posts).where(and(eq(posts.id, id), eq(posts.tenantId, user.tenantId)))
return c.json({ message: 'Post deleted' })
})
export { postsRouter }步骤 6: 创建应用
创建 src/index.ts:
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { initMTPC } from './config/config'
import { authRouter } from './routes/auth'
import { postsRouter } from './routes/posts'
const app = new Hono()
app.use('*', cors())
app.route('/auth', authRouter)
app.route('/posts', postsRouter)
app.get('/', (c) => {
return c.json({ message: 'Blog API' })
})
async function main() {
await initMTPC()
Bun.serve({
fetch: app.fetch,
port: 3000,
})
console.log('Server running on http://localhost:3000')
}
main()步骤 7: 运行应用
# 启动开发服务器
pnpm dev测试 API
注册用户
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "password123",
"name": "John Doe",
"tenantId": "123e4567-e89b-12d3-a456-426614174000"
}'登录
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "password123"
}'创建文章
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"title": "My First Post",
"content": "This is my first blog post",
"status": "published"
}'获取文章列表
curl -X GET http://localhost:3000/posts \
-H "Authorization: Bearer YOUR_TOKEN"总结
本教程展示了如何使用 MTPC 构建一个完整的博客 API,包括:
- 项目初始化和依赖安装
- 数据库配置和 Schema 定义
- 资源定义(用户、文章、评论)
- MTPC 配置和 RBAC 设置
- 路由创建和权限检查
- API 测试
您可以根据需要扩展此项目,添加更多功能,如:
- 文章分类和标签
- 文章搜索
- 评论回复
- 用户关注
- 文章点赞
继续学习: 电商系统教程 →
Last updated on