/**
* Admin Routes
*
* Handles:
* - GET /admin - Admin dashboard
* - GET /api/admin/users - List all users
* - GET /api/admin/identities - List all identities
* - POST /api/admin/users/:userId/permissions - Upgrade permissions
* - POST /api/admin/identities/:identityId/revoke - Revoke identity
* - POST /api/admin/identities/:identityId/restore - Restore identity
* - GET /api/admin/identities/revoked - List revoked identities
* - POST /api/admin/repos/:repoName/visibility - Set visibility
* - POST /api/admin/repos/:repoName/owner - Set owner
* - POST /api/admin/repos/:repoName/collaborators - Add collaborator
* - DELETE /api/admin/repos/:repoName/collaborators/:userId - Remove collaborator
* - POST /api/admin/repos/:repoName/archived - Set archived status
* - DELETE /api/admin/repos/:repoName - Delete repo from registry
*/
import { html, json, unauthorized, forbidden, badRequest, readJson } from '../response.js'
import { requireAdmin } from '../middleware/admin.js'
import { renderAdminDashboard } from '../admin-ui.js'
/**
* Register admin routes
*/
export function registerAdminRoutes(router) {
// Admin dashboard — serve HTML to everyone, JS checks permissions client-side
// API routes below still require admin auth via fetch() headers
router.get('/admin', serveAdminDashboard)
router.get('/admin/', serveAdminDashboard)
// User management
router.get('/api/admin/users', requireAdmin, handleAdminListUsers)
router.get('/api/admin/identities', requireAdmin, handleAdminListIdentities)
router.post('/api/admin/users/:userId/permissions', requireAdmin, handleUpgradePermissions)
// Identity management
router.post('/api/admin/identities/:identityId/revoke', requireAdmin, handleRevokeIdentity)
router.post('/api/admin/identities/:identityId/restore', requireAdmin, handleRestoreIdentity)
router.get('/api/admin/identities/revoked', requireAdmin, handleListRevoked)
// Repository management
router.post('/api/admin/repos/:repoName/visibility', requireAdmin, handleSetRepoVisibility)
router.post('/api/admin/repos/:repoName/owner', requireAdmin, handleSetRepoOwner)
router.post('/api/admin/repos/:repoName/collaborators', requireAdmin, handleAddCollaborator)
router.delete('/api/admin/repos/:repoName/collaborators/:userId', requireAdmin, handleRemoveCollaborator)
router.post('/api/admin/repos/:repoName/archived', requireAdmin, handleSetRepoArchived)
router.delete('/api/admin/repos/:repoName', requireAdmin, handleDeleteRepo)
}
/**
* Serve admin dashboard
*/
async function serveAdminDashboard(req, res, server) {
const { identityRegistry, authManager } = server.auth
const { pathPrefix } = server.config
const { repos } = server.repoData
const { registry } = server.coreData
const federationKey = identityRegistry && identityRegistry.bootstrapKeyHex ? identityRegistry.bootstrapKeyHex : null
const federationDiscoveryKey = identityRegistry && identityRegistry.discoveryKey
? identityRegistry.discoveryKey.toString('hex')
: null
// Count federation peers
let peerCount = 0
try {
for await (const entry of registry.createReadStream({
gt: 'federation:',
lt: 'federation:\xff'
})) {
const data = typeof entry.value === 'string' ? JSON.parse(entry.value) : entry.value
if (data.status === 'active') peerCount++
}
} catch {}
const stats = {
totalUsers: await countUsers(server),
totalRepos: repos.length,
totalIssues: await countIssues(server),
recentActivity: [],
identitySecured: server.identitySecured || false,
federationKey,
federationDiscoveryKey,
peerCount
}
const content = renderAdminDashboard(stats, pathPrefix)
html(res, content)
}
/**
* List all users (admin only)
*/
async function handleAdminListUsers(req, res, server) {
const { authManager } = server.auth
try {
const users = await authManager.identityRegistry.listAllUsers()
json(res, users)
} catch (err) {
console.error('Error listing users:', err)
json(res, { error: 'Failed to list users' }, 500)
}
}
/**
* List all identities (admin only)
*/
async function handleAdminListIdentities(req, res, server) {
const { authManager } = server.auth
try {
const identities = await authManager.identityRegistry.listAllIdentities()
json(res, identities)
} catch (err) {
console.error('Error listing identities:', err)
json(res, { error: 'Failed to list identities' }, 500)
}
}
/**
* Upgrade user permissions
*/
async function handleUpgradePermissions(req, res, server) {
const { authManager } = server.auth
try {
const { userId } = req.params
const { repoName, permissions } = await readJson(req)
if (!repoName || !permissions || !Array.isArray(permissions)) {
badRequest(res, 'repoName and permissions array are required')
return
}
// Validate permissions
const validPermissions = ['read', 'create-issue', 'manage-issues', 'write', 'admin']
const invalidPerms = permissions.filter(p => !validPermissions.includes(p))
if (invalidPerms.length > 0) {
json(res, {
error: `Invalid permissions: ${invalidPerms.join(', ')}`,
validPermissions
}, 400)
return
}
const updatedUser = await authManager.identityRegistry.upgradePermissions(
userId,
repoName,
permissions
)
console.log(`[Admin] ${req.auth.userId || req.auth.identityId} granted [${permissions.join(', ')}] to ${userId} for ${repoName}`)
json(res, {
success: true,
userId: updatedUser.userId,
name: updatedUser.name,
repos: updatedUser.repos
})
} catch (err) {
console.error('[API] Upgrade permissions error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* Revoke identity
*/
async function handleRevokeIdentity(req, res, server) {
const { authManager } = server.auth
try {
const { identityId } = req.params
const { reason } = await readJson(req)
const revokedIdentity = await authManager.revokeIdentity(identityId, {
reason: reason || 'Revoked by admin',
revokedBy: req.auth.userId || req.auth.identityId
})
json(res, {
success: true,
identity: {
identityId: revokedIdentity.identityId,
method: revokedIdentity.method,
revoked: true,
revokedAt: revokedIdentity.revokedAt,
reason: revokedIdentity.revocationReason
}
})
} catch (err) {
console.error('[API] Revoke identity error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* Restore revoked identity
*/
async function handleRestoreIdentity(req, res, server) {
const { authManager } = server.auth
try {
const { identityId } = req.params
const restoredIdentity = await authManager.restoreIdentity(identityId)
json(res, {
success: true,
identity: {
identityId: restoredIdentity.identityId,
method: restoredIdentity.method,
revoked: false,
restoredAt: restoredIdentity.restoredAt
}
})
} catch (err) {
console.error('[API] Restore identity error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* List revoked identities
*/
async function handleListRevoked(req, res, server) {
const { authManager } = server.auth
try {
const revoked = await authManager.listRevokedIdentities()
json(res, {
count: revoked.length,
identities: revoked
})
} catch (err) {
console.error('[API] List revoked error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* Set repository visibility
*/
async function handleSetRepoVisibility(req, res, server) {
const { repoRegistry } = server.repoData
try {
const { repoName } = req.params
const { visibility } = await readJson(req)
await repoRegistry.setVisibility(repoName, visibility)
json(res, { success: true, visibility })
} catch (err) {
console.error('[API] Set repo visibility error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* Set repository owner
*/
async function handleSetRepoOwner(req, res, server) {
const { repoRegistry } = server.repoData
try {
const { repoName } = req.params
const { userId } = await readJson(req)
await repoRegistry.setOwner(repoName, userId)
json(res, { success: true, owner: userId })
} catch (err) {
console.error('[API] Set repo owner error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* Add collaborator
*/
async function handleAddCollaborator(req, res, server) {
const { repoRegistry } = server.repoData
try {
const { repoName } = req.params
const { userId, permissions } = await readJson(req)
await repoRegistry.addCollaborator(repoName, userId, permissions)
json(res, { success: true, userId, permissions })
} catch (err) {
console.error('[API] Add collaborator error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* Remove collaborator
*/
async function handleRemoveCollaborator(req, res, server) {
const { repoRegistry } = server.repoData
try {
const { repoName, userId } = req.params
await repoRegistry.removeCollaborator(repoName, userId)
json(res, { success: true })
} catch (err) {
console.error('[API] Remove collaborator error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* Set repository archived status
*/
async function handleSetRepoArchived(req, res, server) {
const { repoRegistry } = server.repoData
try {
const { repoName } = req.params
const { archived } = await readJson(req)
await repoRegistry.setArchived(repoName, archived)
json(res, { success: true, archived })
} catch (err) {
console.error('[API] Set repo archived error:', err)
json(res, { error: err.message }, 500)
}
}
/**
* Delete repository from registry
*/
async function handleDeleteRepo(req, res, server) {
try {
const { repoName } = req.params
const removed = await server.repoRegistry.unregister(repoName)
console.log(`[Admin] ${req.auth.userId || req.auth.identityId} deleted repo ${repoName} from registry`)
json(res, { success: true, repo: removed.name })
} catch (err) {
console.error('[API] Delete repo error:', err)
json(res, { error: err.message }, err.message.includes('not found') ? 404 : 500)
}
}
// Helper functions
async function countUsers(server) {
const { authManager } = server.auth
try {
const users = await authManager.identityRegistry.listAllUsers()
return users.length
} catch {
return 0
}
}
async function countIssues(server) {
const { repos, linkedRepos } = server.repoData
try {
let total = 0
for (const repo of repos) {
const linked = linkedRepos.get(repo.name)
if (linked) {
const counts = await linked.getIssueCounts()
total += counts.total
}
}
return total
} catch {
return 0
}
}
export {
serveAdminDashboard,
handleAdminListUsers,
handleAdminListIdentities,
handleUpgradePermissions,
handleRevokeIdentity,
handleRestoreIdentity,
handleListRevoked,
handleSetRepoVisibility,
handleSetRepoOwner,
handleAddCollaborator,
handleRemoveCollaborator,
handleSetRepoArchived,
handleDeleteRepo
}