Публичный GET и admin PUT/UI /admin/apps; каталог и chrome читают URL из store. Co-authored-by: Cursor <[email protected]>
226 lines
6.6 KiB
TypeScript
226 lines
6.6 KiB
TypeScript
import type { FastifyInstance } from 'fastify'
|
|
import { hash } from '@node-rs/argon2'
|
|
import {
|
|
createUser,
|
|
deleteUser,
|
|
getAppSwitcherConfig,
|
|
getUserApps,
|
|
getUserByEmail,
|
|
getUserById,
|
|
getUserPermissions,
|
|
listUsers,
|
|
setAppSwitcherConfig,
|
|
setUserAccess,
|
|
updateUser,
|
|
} from '@authportal/db'
|
|
import {
|
|
APP_IDS,
|
|
allPermissionKeys,
|
|
appSwitcherConfigSchema,
|
|
createUserRequestSchema,
|
|
patchUserRequestSchema,
|
|
putUserAccessRequestSchema,
|
|
type AdminUser,
|
|
type AppId,
|
|
} from '@authportal/shared'
|
|
import { requireAdmin } from '../plugins/auth-guards.js'
|
|
|
|
const allowedPermissions = new Set(allPermissionKeys())
|
|
|
|
function mapUser(
|
|
db: FastifyInstance['db'],
|
|
user: NonNullable<ReturnType<typeof getUserById>>,
|
|
): AdminUser {
|
|
const apps = getUserApps(db, user.id).filter((a): a is AppId =>
|
|
(APP_IDS as readonly string[]).includes(a),
|
|
)
|
|
const permissions = getUserPermissions(db, user.id)
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
is_admin: user.isAdmin,
|
|
disabled: user.disabled,
|
|
apps,
|
|
permissions,
|
|
created_at: user.createdAt,
|
|
updated_at: user.updatedAt,
|
|
}
|
|
}
|
|
|
|
function validateAccess(apps: string[], permissions: string[]): string | null {
|
|
for (const app of apps) {
|
|
if (!(APP_IDS as readonly string[]).includes(app)) {
|
|
return `Неизвестное приложение: ${app}`
|
|
}
|
|
}
|
|
for (const p of permissions) {
|
|
if (!allowedPermissions.has(p)) {
|
|
return `Неизвестное право: ${p}`
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
export async function adminRoutes(app: FastifyInstance): Promise<void> {
|
|
app.addHook('onRequest', async (request, reply) => {
|
|
if (!request.url.startsWith('/api/v1/admin')) return
|
|
await requireAdmin(request, reply)
|
|
})
|
|
|
|
app.get('/api/v1/admin/users', async () => {
|
|
return listUsers(app.db).map((u) => mapUser(app.db, u))
|
|
})
|
|
|
|
app.get<{ Params: { id: string } }>(
|
|
'/api/v1/admin/users/:id',
|
|
async (request, reply) => {
|
|
const user = getUserById(app.db, request.params.id)
|
|
if (!user) {
|
|
return reply.status(404).send({
|
|
error: { code: 'NOT_FOUND', message: 'Пользователь не найден' },
|
|
})
|
|
}
|
|
return mapUser(app.db, user)
|
|
},
|
|
)
|
|
|
|
app.post('/api/v1/admin/users', async (request, reply) => {
|
|
const parsed = createUserRequestSchema.safeParse(request.body)
|
|
if (!parsed.success) {
|
|
return reply.status(400).send({
|
|
error: { code: 'VALIDATION_ERROR', message: 'Некорректные данные' },
|
|
})
|
|
}
|
|
const data = parsed.data
|
|
if (getUserByEmail(app.db, data.email)) {
|
|
return reply.status(409).send({
|
|
error: { code: 'CONFLICT', message: 'Email уже занят' },
|
|
})
|
|
}
|
|
const accessError = validateAccess(data.apps, data.permissions)
|
|
if (accessError) {
|
|
return reply.status(400).send({
|
|
error: { code: 'VALIDATION_ERROR', message: accessError },
|
|
})
|
|
}
|
|
|
|
const passwordHash = await hash(data.password)
|
|
const user = createUser(app.db, {
|
|
email: data.email,
|
|
name: data.name,
|
|
passwordHash,
|
|
isAdmin: data.is_admin,
|
|
})
|
|
setUserAccess(app.db, user.id, data.apps, data.permissions)
|
|
return reply.status(201).send(mapUser(app.db, getUserById(app.db, user.id)!))
|
|
})
|
|
|
|
app.patch<{ Params: { id: string } }>(
|
|
'/api/v1/admin/users/:id',
|
|
async (request, reply) => {
|
|
const parsed = patchUserRequestSchema.safeParse(request.body)
|
|
if (!parsed.success) {
|
|
return reply.status(400).send({
|
|
error: { code: 'VALIDATION_ERROR', message: 'Некорректные данные' },
|
|
})
|
|
}
|
|
const existing = getUserById(app.db, request.params.id)
|
|
if (!existing) {
|
|
return reply.status(404).send({
|
|
error: { code: 'NOT_FOUND', message: 'Пользователь не найден' },
|
|
})
|
|
}
|
|
if (parsed.data.email && parsed.data.email !== existing.email) {
|
|
const clash = getUserByEmail(app.db, parsed.data.email)
|
|
if (clash) {
|
|
return reply.status(409).send({
|
|
error: { code: 'CONFLICT', message: 'Email уже занят' },
|
|
})
|
|
}
|
|
}
|
|
|
|
const passwordHash = parsed.data.password
|
|
? await hash(parsed.data.password)
|
|
: undefined
|
|
const updated = updateUser(app.db, request.params.id, {
|
|
email: parsed.data.email,
|
|
name: parsed.data.name,
|
|
passwordHash,
|
|
isAdmin: parsed.data.is_admin,
|
|
disabled: parsed.data.disabled,
|
|
})
|
|
return mapUser(app.db, updated!)
|
|
},
|
|
)
|
|
|
|
app.delete<{ Params: { id: string } }>(
|
|
'/api/v1/admin/users/:id',
|
|
async (request, reply) => {
|
|
if (request.authUser?.id === request.params.id) {
|
|
return reply.status(400).send({
|
|
error: {
|
|
code: 'VALIDATION_ERROR',
|
|
message: 'Нельзя удалить себя',
|
|
},
|
|
})
|
|
}
|
|
const ok = deleteUser(app.db, request.params.id)
|
|
if (!ok) {
|
|
return reply.status(404).send({
|
|
error: { code: 'NOT_FOUND', message: 'Пользователь не найден' },
|
|
})
|
|
}
|
|
return reply.status(204).send()
|
|
},
|
|
)
|
|
|
|
app.put<{ Params: { id: string } }>(
|
|
'/api/v1/admin/users/:id/access',
|
|
async (request, reply) => {
|
|
const parsed = putUserAccessRequestSchema.safeParse(request.body)
|
|
if (!parsed.success) {
|
|
return reply.status(400).send({
|
|
error: { code: 'VALIDATION_ERROR', message: 'Некорректные данные' },
|
|
})
|
|
}
|
|
const existing = getUserById(app.db, request.params.id)
|
|
if (!existing) {
|
|
return reply.status(404).send({
|
|
error: { code: 'NOT_FOUND', message: 'Пользователь не найден' },
|
|
})
|
|
}
|
|
const accessError = validateAccess(
|
|
parsed.data.apps,
|
|
parsed.data.permissions,
|
|
)
|
|
if (accessError) {
|
|
return reply.status(400).send({
|
|
error: { code: 'VALIDATION_ERROR', message: accessError },
|
|
})
|
|
}
|
|
setUserAccess(
|
|
app.db,
|
|
request.params.id,
|
|
parsed.data.apps,
|
|
parsed.data.permissions,
|
|
)
|
|
return mapUser(app.db, getUserById(app.db, request.params.id)!)
|
|
},
|
|
)
|
|
|
|
app.get('/api/v1/admin/app-switcher', async () =>
|
|
getAppSwitcherConfig(app.db),
|
|
)
|
|
|
|
app.put('/api/v1/admin/app-switcher', async (request, reply) => {
|
|
const parsed = appSwitcherConfigSchema.safeParse(request.body)
|
|
if (!parsed.success) {
|
|
return reply.status(400).send({
|
|
error: { code: 'VALIDATION_ERROR', message: 'Некорректные данные' },
|
|
})
|
|
}
|
|
return setAppSwitcherConfig(app.db, parsed.data)
|
|
})
|
|
}
|