SynthQuest admin: - New "🎮 SynthQuest" section in admin sidebar - List custom levels with world, ID, title, patch status - Create new level: world selector, title, subtitle, description, concept (hint), available modules (tag input), boss flag, sort order - Edit existing levels inline - Import patch base from sandbox JSON export (📥 button per level) - Delete levels with confirmation Server: - custom_levels table (PostgreSQL) - CRUD API at /api/v1/admin/levels - POST /:id/import-patch to import sandbox JSON as preplaced modules Admin access: - User badge is now a hover dropdown with "🛠 Admin" + "Cerrar sesion" - Admin visible in Sandbox toolbar, Workshop nav, and user dropdown - onSwitchToAdmin passed through navigation chain Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
104 lines
3.2 KiB
JavaScript
104 lines
3.2 KiB
JavaScript
const API_BASE = '/api/v1';
|
|
|
|
let _accessToken = null;
|
|
let _onUnauthorized = null;
|
|
|
|
export function setAccessToken(token) { _accessToken = token; }
|
|
export function getAccessToken() { return _accessToken; }
|
|
export function setOnUnauthorized(fn) { _onUnauthorized = fn; }
|
|
|
|
async function request(method, path, body = null, opts = {}) {
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (_accessToken) headers['Authorization'] = `Bearer ${_accessToken}`;
|
|
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
method,
|
|
headers,
|
|
credentials: 'include', // send cookies (refresh token)
|
|
body: body ? JSON.stringify(body) : null,
|
|
...opts,
|
|
});
|
|
|
|
if (res.status === 401 && !path.includes('/auth/')) {
|
|
// Try to refresh
|
|
const refreshed = await refreshToken();
|
|
if (refreshed) {
|
|
headers['Authorization'] = `Bearer ${_accessToken}`;
|
|
const retry = await fetch(`${API_BASE}${path}`, {
|
|
method, headers, credentials: 'include',
|
|
body: body ? JSON.stringify(body) : null,
|
|
});
|
|
if (retry.ok) return retry.json();
|
|
}
|
|
_onUnauthorized?.();
|
|
throw new Error('Unauthorized');
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
throw new Error(data.error || `HTTP ${res.status}`);
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
async function refreshToken() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/auth/refresh`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
if (!res.ok) return false;
|
|
const data = await res.json();
|
|
_accessToken = data.accessToken;
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Auth
|
|
export const auth = {
|
|
register: (email, username, password) =>
|
|
request('POST', '/auth/register', { email, username, password }),
|
|
login: (email, password) =>
|
|
request('POST', '/auth/login', { email, password }),
|
|
logout: () => request('POST', '/auth/logout'),
|
|
refresh: refreshToken,
|
|
};
|
|
|
|
// Users
|
|
export const users = {
|
|
me: () => request('GET', '/users/me'),
|
|
update: (data) => request('PATCH', '/users/me', data),
|
|
};
|
|
|
|
// Workshop
|
|
export const workshop = {
|
|
browse: (params = '') => request('GET', `/workshop${params ? '?' + params : ''}`),
|
|
get: (id) => request('GET', `/workshop/${id}`),
|
|
share: (data) => request('POST', '/workshop', data),
|
|
remove: (id) => request('DELETE', `/workshop/${id}`),
|
|
like: (id) => request('POST', `/workshop/${id}/like`),
|
|
unlike: (id) => request('DELETE', `/workshop/${id}/like`),
|
|
report: (id) => request('POST', `/workshop/${id}/report`),
|
|
};
|
|
|
|
// Admin Levels
|
|
export const levels = {
|
|
list: () => request('GET', '/admin/levels'),
|
|
create: (data) => request('POST', '/admin/levels', data),
|
|
update: (id, data) => request('PATCH', `/admin/levels/${id}`, data),
|
|
remove: (id) => request('DELETE', `/admin/levels/${id}`),
|
|
importPatch: (id, patchData) => request('POST', `/admin/levels/${id}/import-patch`, patchData),
|
|
};
|
|
|
|
// Admin
|
|
export const admin = {
|
|
stats: () => request('GET', '/admin/stats'),
|
|
users: (params = '') => request('GET', `/admin/users${params ? '?' + params : ''}`),
|
|
updateUser: (id, data) => request('PATCH', `/admin/users/${id}`, data),
|
|
patches: (params = '') => request('GET', `/admin/patches${params ? '?' + params : ''}`),
|
|
updatePatch: (id, data) => request('PATCH', `/admin/patches/${id}`, data),
|
|
};
|