码桶
发现社区成员的开源项目
users.js7.9 KB
// User management API (admin only, enforced by middleware)
import { logAudit } from '../_audit.js';
import { fireWebhook } from '../_webhook.js';
function generateSetupToken() {
const arr = new Uint8Array(24);
crypto.getRandomValues(arr);
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
}
// GET: List all users
export async function onRequestGet(context) {
const { env } = context;
const kv = env.CF_DNS_KV;
if (!kv) {
return new Response(JSON.stringify({ error: 'KV storage not configured.' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
const users = [{ username: 'admin', role: 'admin', status: 'active', createdAt: null }];
const listJson = await kv.get('USER_LIST');
const userList = listJson ? JSON.parse(listJson) : [];
for (const uname of userList) {
const dataJson = await kv.get(`USER:${uname}`);
if (dataJson) {
const data = JSON.parse(dataJson);
users.push({
username: data.username,
role: data.role,
status: data.status || 'active',
createdAt: data.createdAt,
hasSetupToken: !!data.setupToken,
allowedZones: Array.isArray(data.allowedZones) ? data.allowedZones : []
});
}
}
return new Response(JSON.stringify({ users }), {
headers: { 'Content-Type': 'application/json' }
});
}
// POST: Create a new user (no password — generates setup token)
export async function onRequestPost(context) {
const { env } = context;
const kv = env.CF_DNS_KV;
if (!kv) {
return new Response(JSON.stringify({ error: 'KV storage not configured.' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
const body = await context.request.json();
const { username, role, allowedZones } = body;
if (!username || !username.trim()) {
return new Response(JSON.stringify({ error: 'Username is required.' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const cleanUsername = username.trim().toLowerCase();
if (cleanUsername === 'admin') {
return new Response(JSON.stringify({ error: 'Cannot create user with username "admin".' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
if (!/^[a-z0-9_-]+$/.test(cleanUsername)) {
return new Response(JSON.stringify({ error: 'Username must contain only lowercase letters, numbers, hyphens, and underscores.' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const existing = await kv.get(`USER:${cleanUsername}`);
if (existing) {
return new Response(JSON.stringify({ error: 'User already exists.' }), {
status: 409,
headers: { 'Content-Type': 'application/json' }
});
}
const setupToken = generateSetupToken();
const parsedAllowedZones = Array.isArray(allowedZones)
? allowedZones.filter(z => typeof z === 'string' && z.trim()).map(z => z.trim().toLowerCase())
: [];
const userData = {
username: cleanUsername,
passwordHash: null,
setupToken,
status: 'pending',
role: role === 'admin' ? 'admin' : 'user',
allowedZones: parsedAllowedZones,
createdAt: new Date().toISOString()
};
await kv.put(`USER:${cleanUsername}`, JSON.stringify(userData));
await kv.put(`USER_TOKENS:${cleanUsername}`, JSON.stringify([]));
const listJson = await kv.get('USER_LIST');
const userList = listJson ? JSON.parse(listJson) : [];
if (!userList.includes(cleanUsername)) {
userList.push(cleanUsername);
await kv.put('USER_LIST', JSON.stringify(userList));
}
const adminUser = context.data.user?.username || 'admin';
await logAudit(kv, adminUser, 'user.create', `Created user: ${cleanUsername} (role: ${userData.role})`);
await fireWebhook(kv, {
type: 'user.create',
username: adminUser,
detail: `Created user: ${cleanUsername} (role: ${userData.role})`
});
return new Response(JSON.stringify({ success: true, username: cleanUsername, setupToken }), {
headers: { 'Content-Type': 'application/json' }
});
}
// PUT: Update a user (role only — admin cannot change passwords)
export async function onRequestPut(context) {
const { env } = context;
const kv = env.CF_DNS_KV;
if (!kv) {
return new Response(JSON.stringify({ error: 'KV storage not configured.' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
const body = await context.request.json();
const { username, role, resetSetupToken, allowedZones } = body;
if (!username || username === 'admin') {
return new Response(JSON.stringify({ error: 'Cannot modify the admin user.' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const dataJson = await kv.get(`USER:${username}`);
if (!dataJson) {
return new Response(JSON.stringify({ error: 'User not found.' }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
const userData = JSON.parse(dataJson);
if (role) {
userData.role = role === 'admin' ? 'admin' : 'user';
}
// Update allowed zones if provided
if (allowedZones !== undefined) {
userData.allowedZones = Array.isArray(allowedZones)
? allowedZones.filter(z => typeof z === 'string' && z.trim()).map(z => z.trim().toLowerCase())
: [];
}
// Admin can regenerate setup token (resets user to pending)
let newSetupToken = null;
if (resetSetupToken) {
newSetupToken = generateSetupToken();
userData.setupToken = newSetupToken;
userData.passwordHash = null;
userData.status = 'pending';
}
await kv.put(`USER:${username}`, JSON.stringify(userData));
const adminUser = context.data.user?.username || 'admin';
await logAudit(kv, adminUser, 'user.update', `Updated user: ${username}${role ? ' role=' + userData.role : ''}${resetSetupToken ? ' (reset setup token)' : ''}`);
const resp = { success: true };
if (newSetupToken) resp.setupToken = newSetupToken;
return new Response(JSON.stringify(resp), {
headers: { 'Content-Type': 'application/json' }
});
}
// DELETE: Delete a user
export async function onRequestDelete(context) {
const { env } = context;
const kv = env.CF_DNS_KV;
if (!kv) {
return new Response(JSON.stringify({ error: 'KV storage not configured.' }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
const url = new URL(context.request.url);
const username = url.searchParams.get('username');
if (!username || username === 'admin') {
return new Response(JSON.stringify({ error: 'Cannot delete the admin user.' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
await kv.delete(`USER:${username}`);
await kv.delete(`USER_TOKENS:${username}`);
const listJson = await kv.get('USER_LIST');
const userList = listJson ? JSON.parse(listJson) : [];
const filtered = userList.filter(u => u !== username);
await kv.put('USER_LIST', JSON.stringify(filtered));
const adminUser = context.data.user?.username || 'admin';
await logAudit(kv, adminUser, 'user.delete', `Deleted user: ${username}`);
await fireWebhook(kv, {
type: 'user.delete',
username: adminUser,
detail: `Deleted user: ${username}`
});
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' }
});
}