/**
* Authentication Middleware
*
* Provides middleware functions for route authentication
*/
import { unauthorized, forbidden, json } from '../response.js'
/**
* Require authentication (any valid identity)
* Attaches auth result to req.auth
* Returns 401 if not authenticated
*/
export async function requireAuth(req, res, server) {
if (!server.authEnabled) {
// Auth disabled - allow all requests
req.auth = null
return true
}
const auth = await server.authManager.authenticate(req)
if (!auth) {
unauthorized(res, 'Authentication required')
return false
}
req.auth = auth
return true
}
/**
* Optional authentication - attaches auth if available, continues either way
* Always returns true (never blocks request)
*/
export async function optionalAuth(req, res, server) {
if (!server.authEnabled) {
req.auth = null
return true
}
try {
req.auth = await server.authManager.authenticate(req)
} catch (err) {
// Silently fail - optional auth
req.auth = null
}
return true
}
/**
* Require repo access for a specific action
* Must be used after requireAuth or optionalAuth
*
* @param {string} action - 'read' or 'write'
*/
export function requireRepoAccess(action) {
return async function (req, res, server) {
const { repoName } = req.params
if (!repoName) {
// No repo in route - skip check
return true
}
// Get repo info
const repo = server.repos.find(r => r.name === repoName)
if (!repo) {
json(res, { error: 'Repository not found' }, 404)
return false
}
// Read access is always allowed (federation = full read)
if (action === 'read') {
req.repo = repo
return true
}
// Write access requires auth
if (!req.auth) {
unauthorized(res, 'Authentication required')
return false
}
// Check permissions — pass registry context for grant-based access
const serverKey = server.serverIdentity
? server.serverIdentity.getPublicKeyHex()
: null
const hasAccess = await server.authManager.hasRepoAccess(req.auth, repo, action, {
namespaceRegistry: server.namespaceRegistry,
serverKey
})
if (!hasAccess) {
forbidden(res, `${action} access denied for repository: ${repoName}`)
return false
}
req.repo = repo
return true
}
}
/**
* Require write permission (any valid write access)
* For operations that modify data
*/
export async function requireWrite(req, res, server) {
if (!server.authEnabled) {
req.auth = null
return true
}
const auth = await server.authManager.authenticate(req)
if (!auth) {
unauthorized(res, 'Authentication required')
return false
}
if (!auth.permissions.write && !auth.permissions.admin) {
forbidden(res, 'Write permission required')
return false
}
req.auth = auth
return true
}
/**
* Load user info from auth
* Must be used after requireAuth
* Attaches full user info to req.user
*/
export async function loadUser(req, res, server) {
if (!req.auth) {
return true
}
try {
const userResult = await server.authManager.getUserWithIdentities(req.auth.identityId)
if (userResult) {
req.user = userResult.user
req.identities = userResult.identities
}
} catch (err) {
console.error('Error loading user:', err)
}
return true
}
/**
* Check if user has specific permission
* @param {string|string[]} permissions - Permission(s) to check
*/
export function requirePermission(...permissions) {
return async function (req, res, server) {
if (!server.authEnabled) {
return true
}
if (!req.auth) {
unauthorized(res, 'Authentication required')
return false
}
// Load user if not already loaded
if (!req.user) {
const userResult = await server.authManager.getUserWithIdentities(req.auth.identityId)
if (!userResult) {
forbidden(res, 'User not found')
return false
}
req.user = userResult.user
}
// Check global permissions
const hasPermission = permissions.some(perm =>
req.user.globalPermissions.includes(perm) ||
req.user.globalPermissions.includes('admin')
)
if (!hasPermission) {
forbidden(res, `Permission required: ${permissions.join(' or ')}`)
return false
}
return true
}
}