builder / app /api /admin /users /route.ts
Leon4gr45's picture
Upload folder using huggingface_hub
eeb9404 verified
Raw
History Blame Contribute Delete
6.38 kB
/**
* Admin Users API
* GET /api/admin/users - List all users (with per-user stats)
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, verifyInstanceApiKey } from '@/lib/auth/session';
import { listUsers, listUserWorkspaces, createUser, getUserByEmail, createWorkspace, setDefaultWorkspace } from '@/lib/auth/system-database';
import { hashPassword } from '@/lib/auth/passwords';
import Database from 'better-sqlite3';
import path from 'path';
import fs from 'fs';
function getDataDir(): string {
return process.env.DATA_DIR || path.join(process.cwd(), 'data');
}
/**
* Aggregate project count + last active date across all of a user's workspaces.
*/
function getUserStats(userId: string): { projectCount: number; lastActive: string | null } {
const workspaces = listUserWorkspaces(userId);
let totalProjects = 0;
let lastActive: string | null = null;
for (const ws of workspaces) {
const dbPath = path.join(getDataDir(), 'workspaces', ws.id, 'osws.sqlite');
if (!fs.existsSync(dbPath)) continue;
try {
const db = new Database(dbPath, { readonly: true });
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get();
if (tableExists) {
totalProjects += (db.prepare('SELECT COUNT(*) as count FROM projects').get() as { count: number }).count;
const lastProject = db.prepare('SELECT updated_at FROM projects ORDER BY updated_at DESC LIMIT 1').get() as { updated_at: string } | undefined;
if (lastProject?.updated_at && (!lastActive || lastProject.updated_at > lastActive)) {
lastActive = lastProject.updated_at;
}
}
db.close();
} catch { /* skip inaccessible workspace DB */ }
}
return { projectCount: totalProjects, lastActive };
}
/**
* Recursively compute total size of a directory in bytes.
*/
function getDirectorySizeSync(dirPath: string): number {
let total = 0;
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
total += getDirectorySizeSync(fullPath);
} else if (entry.isFile()) {
total += fs.statSync(fullPath).size;
}
}
} catch { /* directory may not exist */ }
return total;
}
/**
* Calculate total storage for a user by summing all workspace data directories.
*/
function getUserStorageMb(userId: string): number {
const workspaces = listUserWorkspaces(userId);
let totalBytes = 0;
for (const ws of workspaces) {
const wsDir = path.join(getDataDir(), 'workspaces', ws.id);
totalBytes += getDirectorySizeSync(wsDir);
}
return Math.round((totalBytes / (1024 * 1024)) * 10) / 10;
}
export async function GET(request: NextRequest) {
try {
const apiSession = verifyInstanceApiKey(request);
const session = apiSession || await requireAuth();
if (!session.isAdmin) {
return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
}
const users = listUsers();
// Enrich with workspace info, project stats, and storage (exclude password hash from response)
const enriched = users.map(user => {
const stats = getUserStats(user.id);
return {
id: user.id,
email: user.email,
displayName: user.display_name,
isAdmin: user.is_admin === 1,
active: user.active === 1,
workspaces: listUserWorkspaces(user.id),
projectCount: stats.projectCount,
storageMb: getUserStorageMb(user.id),
lastActive: stats.lastActive,
createdAt: user.created_at,
updatedAt: user.updated_at,
};
});
return NextResponse.json({ users: enriched });
} catch (error) {
if (error instanceof Error && error.message === 'Unauthorized') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.json({ error: 'Failed to list users' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const apiSession = verifyInstanceApiKey(request);
const session = apiSession || await requireAuth();
if (!session.isAdmin) {
return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
}
const body = await request.json();
const { email, password, displayName, workspaceAssignment, workspaceId: assignWorkspaceId, isAdmin: makeAdmin } = body;
if (!email || !password) {
return NextResponse.json({ error: 'Email and password are required' }, { status: 400 });
}
// Check for existing user
const existing = getUserByEmail(email);
if (existing) {
return NextResponse.json({ error: 'A user with this email already exists' }, { status: 409 });
}
const passwordHash = await hashPassword(password);
const userId = createUser(email, passwordHash, displayName || undefined);
// Optionally promote to instance admin
if (makeAdmin) {
const { updateUser } = await import('@/lib/auth/system-database');
updateUser(userId, { active: 1 }); // updateUser doesn't support is_admin directly
const db = (await import('@/lib/auth/system-database')).getSystemDatabase();
db.prepare("UPDATE users SET is_admin = 1 WHERE id = ?").run(userId);
}
// Workspace assignment
let workspaceId: string | undefined;
if (workspaceAssignment === 'existing' && assignWorkspaceId) {
const { grantWorkspaceAccess } = await import('@/lib/auth/system-database');
grantWorkspaceAccess(userId, assignWorkspaceId, 'editor');
setDefaultWorkspace(userId, assignWorkspaceId);
workspaceId = assignWorkspaceId;
} else if (workspaceAssignment !== 'none') {
// Default: create new workspace
const workspaceName = displayName ? `${displayName}'s Workspace` : 'My Workspace';
workspaceId = createWorkspace(workspaceName, userId);
setDefaultWorkspace(userId, workspaceId);
}
return NextResponse.json({ id: userId, workspaceId }, { status: 201 });
} catch (error) {
if (error instanceof Error && error.message === 'Unauthorized') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.json({ error: 'Failed to create user' }, { status: 500 });
}
}