OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / routes / admin.js

387 lines · javascript ref:
/**
 * 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
}