First of the five domain-split PRs in docs/website/API_V2_PLAN.md § Phase 2. Pure
mechanical re-wiring: routes move between files, no handler, gate, validator or
annotation changes, and not one URL moves.
New src/router/v1/admin/index.js owns the two things the group shares — the
`noindex, isLoggedIn, staffOnly` gate and the mount table — and declares no routes
itself. The gate sits ahead of every mount so a capability router extracted in a
later PR cannot silently ship without it. Four capability routers mount at the
prefix they already owned inside the monolith:
account.router.js 6 routes -> /admin/account (self-service, no adminOnly)
users.router.js 15 routes -> /admin/users (adminOnly, router-level)
invites.router.js 3 routes -> /admin/invites (adminOnly, per-route)
authProviders.router.js 4 routes -> /admin/auth (adminOnly, per-route)
admin.routes.js keeps the other 82 (6+15+3+4+82 = the 110 inventoried admin
routes) and is mounted last at the group root; none of the four prefixes appears
in it, so nothing depends on mount ordering. It disappears when PR 5 lands.
Handlers still live in admin.controller.js and usersShard.controller.js — this
re-wires routes, not logic. `adminOnly` moves with the routes that use it, and
`usersRouter.use(adminOnly)` is exactly equivalent to the old
`adminRouter.use('/users', adminOnly)` now that the router is mounted at /users.
All three generated gates are zero-diff:
routes.manifest.json unchanged (200 public + 2 internal)
routes.guards.json unchanged — no route lost or gained a gate
swagger-output.json unchanged, byte-for-byte
The spec staying byte-identical depends on the path normalization landed in the
preceding commit; without it the four collection routes would have documented as
/api/v1/admin/{users,invites,account}/ with a trailing slash.
Server tests green (434/434).
Co-Authored-By: Claude <noreply@anthropic.com>
103 lines
6.8 KiB
JavaScript
103 lines
6.8 KiB
JavaScript
// Admin · Auth Providers — SSO/OAuth2/OIDC provider configuration.
|
|
//
|
|
// Mounted at /api/v1/admin/auth by admin/index.js, which already applied
|
|
// `noindex, isLoggedIn, staffOnly`; the routes below are /providers under that,
|
|
// so the emitted URLs stay /api/v1/admin/auth/providers[/:id].
|
|
//
|
|
// Admin-only: these rows carry client secrets (write-only, AES-GCM at rest via
|
|
// utils/secretBox.js) and decide which external identities may sign in at all.
|
|
|
|
const express = require('express')
|
|
const { body, param } = require('express-validator')
|
|
|
|
const authProviders = require('./authProviders.controller')
|
|
const { requireRole } = require('../../../utils/auth')
|
|
const validate = require('../../../middleware/validate')
|
|
|
|
const providersRouter = express.Router()
|
|
const adminOnly = requireRole('admin')
|
|
|
|
providersRouter.get(
|
|
'/providers',
|
|
// #swagger.tags = ['Admin · Auth Providers']
|
|
// #swagger.summary = 'List configured SSO providers (admin only)'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
/* #swagger.responses[200] = { description: 'Providers (secrets stripped)', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ProviderConfig" } } } } } */
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
adminOnly,
|
|
authProviders.list,
|
|
)
|
|
providersRouter.post(
|
|
'/providers',
|
|
// #swagger.tags = ['Admin · Auth Providers']
|
|
// #swagger.summary = 'Create a custom SSO provider (admin only)'
|
|
// #swagger.description = 'Built-in providers (google, discord) are configured via PUT, not created here.'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderCreateRequest" } } } } */
|
|
/* #swagger.responses[201] = { description: 'Created provider', content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderConfig" } } } } */
|
|
/* #swagger.responses[400] = { description: 'Validation error, or a built-in/invalid kind', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[409] = { description: 'Provider id already exists', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
adminOnly,
|
|
body('id').matches(/^[a-z0-9-]+$/),
|
|
body('kind').isIn(['oidc', 'oauth2']),
|
|
body('name').isString().trim().notEmpty().isLength({ max: 80 }),
|
|
body('enabled').optional().isBoolean(),
|
|
body('clientId').optional({ values: 'falsy' }).isString(),
|
|
body('secret').optional({ values: 'falsy' }).isString(),
|
|
body('authorizeUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
|
|
body('tokenUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
|
|
body('userinfoUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
|
|
body('scopes').optional({ values: 'falsy' }).isString().isLength({ max: 500 }),
|
|
body('priority').optional().isInt(),
|
|
validate,
|
|
authProviders.create,
|
|
)
|
|
providersRouter.put(
|
|
'/providers/:id',
|
|
// #swagger.tags = ['Admin · Auth Providers']
|
|
// #swagger.summary = 'Update an SSO provider (admin only)'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
|
|
/* #swagger.requestBody = { content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderCreateRequest" } } } } */
|
|
/* #swagger.responses[200] = { description: 'Updated provider', content: { "application/json": { schema: { $ref: "#/components/schemas/ProviderConfig" } } } } */
|
|
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[404] = { description: 'Provider not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
adminOnly,
|
|
param('id').matches(/^[a-z0-9-]+$/),
|
|
body('name').optional().isString().trim().notEmpty().isLength({ max: 80 }),
|
|
body('enabled').optional().isBoolean(),
|
|
body('clientId').optional({ values: 'falsy' }).isString(),
|
|
body('secret').optional({ values: 'falsy' }).isString(),
|
|
body('authorizeUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
|
|
body('tokenUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
|
|
body('userinfoUrl').optional({ values: 'falsy' }).isURL({ require_tld: false }),
|
|
body('scopes').optional({ values: 'falsy' }).isString().isLength({ max: 500 }),
|
|
body('priority').optional().isInt(),
|
|
validate,
|
|
authProviders.update,
|
|
)
|
|
providersRouter.delete(
|
|
'/providers/:id',
|
|
// #swagger.tags = ['Admin · Auth Providers']
|
|
// #swagger.summary = 'Delete a custom SSO provider (admin only)'
|
|
// #swagger.description = 'Built-in providers cannot be deleted — disable them instead.'
|
|
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
|
|
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id.' }
|
|
/* #swagger.responses[200] = { description: 'Deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/DeletedFlag" } } } } */
|
|
/* #swagger.responses[400] = { description: 'Built-in provider cannot be deleted', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[404] = { description: 'Provider not found', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
adminOnly,
|
|
param('id').matches(/^[a-z0-9-]+$/),
|
|
validate,
|
|
authProviders.remove,
|
|
)
|
|
|
|
module.exports = providersRouter
|