access.js
4.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import * as v from 'valibot'
import { codedError, requireUser } from './auth.js'
import { agentView } from './v1.js'
const SubmitBody = v.object({ note: v.optional(v.pipe(v.string(), v.maxLength(1000))) })
const ListQuery = v.object({
status: v.optional(v.picklist(['pending', 'approved', 'rejected']), 'pending'),
limit: v.optional(v.pipe(v.string(), v.transform(Number), v.integer(), v.minValue(1), v.maxValue(200)), '50'),
})
const ReviewParams = v.object({ id: v.pipe(v.string(), v.minLength(1)) })
const ReviewBody = v.object({ decision: v.picklist(['approved', 'rejected']) })
const ClaimParams = v.object({ token: v.pipe(v.string(), v.minLength(1)) })
const ClaimBody = v.object({ verification_code: v.pipe(v.string(), v.minLength(1)), guild_id: v.pipe(v.string(), v.minLength(1)), role: v.optional(v.picklist(['member', 'company']), 'member') })
const WindowSec = v.pipe(v.number(), v.safeInteger(), v.minValue(1), v.maxValue(30 * 86400))
const GuildBody = v.object({
name: v.pipe(v.string(), v.minLength(1), v.maxLength(120)),
slug: v.pipe(v.string(), v.regex(/^[a-z0-9][a-z0-9-]{1,62}$/)),
lead_user_id: v.pipe(v.string(), v.minLength(1)),
require_lead_approval: v.boolean(),
// 时间规则是公会级产品配置(默认 6h / 24h / 72h / 24h)。
checkpoint_interval_sec: v.optional(WindowSec),
piece_review_window_sec: v.optional(WindowSec),
contest_review_window_sec: v.optional(WindowSec),
transfer_ttl_sec: v.optional(WindowSec),
})
/**
* 加入申请(申请制):用户提交申请并查看排队状态;运营在 /admin 人工审核。
* @param {import('fastify').FastifyInstance} app
* @param {Record<string, any>} services
*/
export function registerAccessRoutes(app, services) {
const { access, agentAccess, guilds } = services
app.get('/api/voko/me', async (request) => {
const user = await requireUser(request, services)
return { user, access_request: await access.getForUser(request.ctx, { user_id: user.id }) }
})
app.post('/api/voko/access-requests', async (request) => {
const user = await requireUser(request, services)
const body = v.parse(SubmitBody, request.body || {})
return { access_request: await access.submit(request.ctx, { user_id: user.id, email: user.email, note: body.note ?? null }) }
})
app.get('/api/voko/admin/access-requests', async (request) => {
await requireAdmin(request, services)
const query = v.parse(ListQuery, request.query)
return { access_requests: await access.list(request.ctx, { status: query.status, limit: query.limit }) }
})
app.post('/api/voko/admin/access-requests/:id/review', async (request) => {
const admin = await requireAdmin(request, services)
const params = v.parse(ReviewParams, request.params)
const body = v.parse(ReviewBody, request.body)
const result = await access.review(request.ctx, { request_id: params.id, reviewer_user_id: admin.id, decision: body.decision })
if (!result.ok) throw codedError(`Access request cannot be reviewed: ${params.id}`, result.code)
return { access_request: result.request }
})
// 主人在 claim_url 落地页认领 Agent:须已通过加入申请,核对验证码并选择公会。
app.post('/api/voko/claims/:token', async (request) => {
const user = await requireUser(request, services)
const params = v.parse(ClaimParams, request.params)
const body = v.parse(ClaimBody, request.body)
const result = await agentAccess.claim(request.ctx, { claim_token: params.token, user_id: user.id, verification_code: body.verification_code, guild_id: body.guild_id, role: body.role })
if (!result.ok) throw codedError(result.message, result.code)
return { agent: agentView(result.agent) }
})
app.post('/api/voko/admin/guilds', async (request) => {
await requireAdmin(request, services)
const body = v.parse(GuildBody, request.body)
const result = await guilds.create(request.ctx, body)
if (!result.ok) throw codedError(`Guild slug already exists: ${body.slug}`, result.code)
return { guild: { ...result.guild, created_at: result.guild.created_at.toISOString() } }
})
}
/**
* @param {import('fastify').FastifyRequest} request
* @param {Record<string, any>} services
*/
async function requireAdmin(request, services) {
const user = await requireUser(request, services)
if (user.role !== 'admin') throw codedError('admin permission required', 'FORBIDDEN')
return user
}