import path from 'node:path'; import { loadEnv, sendJson, readJsonBody, parseCookies, setCookie, signSession, verifySession, clientIp, matchRoute, serveStatic, listen, dirFromMeta, } from '../lib/node-server.js'; const __dirname = dirFromMeta(import.meta.url); loadEnv(path.join(__dirname, '.env')); const PORT = Number(process.env.ADMIN_UI_PORT || 3000); const HOST = process.env.ADMIN_UI_HOST || '127.0.0.1'; const USE_HTTP2 = process.env.HTTP2 === 'true'; const NEXTCLOUD_URL = (process.env.NEXTCLOUD_URL || 'http://127.0.0.1:8080').replace(/\/$/, ''); const SESSION_SECRET = process.env.SESSION_SECRET || 'dev-secret-change-me'; const SESSION_COOKIE = 'nc_admin'; const PUBLIC_DIR = path.join(__dirname, 'public'); const ALLOWED_IPS = (process.env.ALLOWED_IPS || '127.0.0.1,::1,::ffff:127.0.0.1') .split(',').map((s) => s.trim()).filter(Boolean); const TLS = process.env.SSL_KEY && process.env.SSL_CERT ? { key: process.env.SSL_KEY, cert: process.env.SSL_CERT } : null; function getSession(req) { const cookies = parseCookies(req); return verifySession(cookies[SESSION_COOKIE], SESSION_SECRET); } function authHeader(user, pass) { return 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64'); } async function ocsRequest(user, pass, method, ocsPath, { body, searchParams } = {}) { const url = new URL(`${NEXTCLOUD_URL}${ocsPath}`); if (searchParams) { for (const [k, v] of Object.entries(searchParams)) url.searchParams.set(k, v); } const headers = { 'OCS-APIRequest': 'true', Accept: 'application/json', Authorization: authHeader(user, pass), }; const init = { method, headers }; if (body && method !== 'GET') { headers['Content-Type'] = 'application/x-www-form-urlencoded'; init.body = new URLSearchParams(body).toString(); } const res = await fetch(url, init); const data = JSON.parse(await res.text()); const meta = data?.ocs?.meta; if (!meta || meta.status !== 'ok') { throw new Error(meta?.message || `OCS error ${meta?.statuscode || res.status}`); } return data.ocs.data; } const routes = [ { method: 'POST', path: '/api/login', handler: async (req, res) => { const { username, password } = await readJsonBody(req); if (!username || !password) return sendJson(res, 400, { error: 'Укажите логин и пароль' }); try { await ocsRequest(username, password, 'GET', '/ocs/v1.php/cloud/users', { searchParams: { offset: 0, limit: 1 }, }); setCookie(res, SESSION_COOKIE, signSession({ ncUser: username, ncPass: password }, SESSION_SECRET), { maxAge: 8 * 3600, }); sendJson(res, 200, { ok: true, username }); } catch { sendJson(res, 401, { error: 'Неверный логин/пароль или нет прав администратора' }); } }, }, { method: 'POST', path: '/api/logout', handler: async (_req, res) => { setCookie(res, SESSION_COOKIE, '', { clear: true }); sendJson(res, 200, { ok: true }); }, }, { method: 'GET', path: '/api/me', handler: async (req, res) => { const s = getSession(req); if (!s?.ncUser) return sendJson(res, 401, { error: 'Не авторизован' }); sendJson(res, 200, { username: s.ncUser, nextcloudUrl: NEXTCLOUD_URL }); }, }, { method: 'GET', path: '/api/groups', handler: async (req, res) => { const s = getSession(req); if (!s?.ncUser) return sendJson(res, 401, { error: 'Требуется вход' }); try { const data = await ocsRequest(s.ncUser, s.ncPass, 'GET', '/ocs/v1.php/cloud/groups', { searchParams: { offset: 0, limit: 500 }, }); sendJson(res, 200, { groups: data.groups || [] }); } catch (e) { sendJson(res, 500, { error: e.message }); } }, }, { method: 'GET', path: '/api/users', handler: async (req, res) => { const s = getSession(req); if (!s?.ncUser) return sendJson(res, 401, { error: 'Требуется вход' }); try { const list = await ocsRequest(s.ncUser, s.ncPass, 'GET', '/ocs/v1.php/cloud/users', { searchParams: { offset: 0, limit: 500 }, }); const users = await Promise.all( (list.users || []).map(async (id) => { try { const info = await ocsRequest(s.ncUser, s.ncPass, 'GET', `/ocs/v1.php/cloud/users/${encodeURIComponent(id)}`); const groups = await ocsRequest(s.ncUser, s.ncPass, 'GET', `/ocs/v1.php/cloud/users/${encodeURIComponent(id)}/groups`); const enabled = await ocsRequest(s.ncUser, s.ncPass, 'GET', `/ocs/v1.php/cloud/users/${encodeURIComponent(id)}/enabled`); return { id, displayName: info.displayname || id, email: info.email || '', quota: info.quota?.quota || info.quota || 'default', used: info.quota?.used || 0, free: info.quota?.free || 0, groups: groups.groups || [], enabled: enabled === 1 || enabled === '1' || enabled === true, }; } catch { return { id, displayName: id, email: '', quota: '?', groups: [], enabled: true }; } }) ); sendJson(res, 200, { users }); } catch (e) { sendJson(res, 500, { error: e.message }); } }, }, { method: 'POST', path: '/api/users', handler: async (req, res) => { const s = getSession(req); if (!s?.ncUser) return sendJson(res, 401, { error: 'Требуется вход' }); const { userid, password, quota, groups } = await readJsonBody(req); if (!userid || !password) return sendJson(res, 400, { error: 'userid и password обязательны' }); try { await ocsRequest(s.ncUser, s.ncPass, 'POST', '/ocs/v1.php/cloud/users', { body: { userid, password } }); if (quota) { await ocsRequest(s.ncUser, s.ncPass, 'PUT', `/ocs/v1.php/cloud/users/${encodeURIComponent(userid)}`, { body: { key: 'quota', value: quota }, }); } for (const g of groups || []) { if (g) { await ocsRequest(s.ncUser, s.ncPass, 'POST', `/ocs/v1.php/cloud/users/${encodeURIComponent(userid)}/groups`, { body: { groupid: g }, }); } } sendJson(res, 200, { ok: true }); } catch (e) { sendJson(res, 400, { error: e.message }); } }, }, { method: 'PATCH', path: '/api/users/:id', handler: async (req, res, params) => { const s = getSession(req); if (!s?.ncUser) return sendJson(res, 401, { error: 'Требуется вход' }); const { quota, password, displayName, email, enabled } = await readJsonBody(req); const id = params.id; try { if (quota !== undefined) { await ocsRequest(s.ncUser, s.ncPass, 'PUT', `/ocs/v1.php/cloud/users/${encodeURIComponent(id)}`, { body: { key: 'quota', value: quota }, }); } if (password) { await ocsRequest(s.ncUser, s.ncPass, 'PUT', `/ocs/v1.php/cloud/users/${encodeURIComponent(id)}`, { body: { key: 'password', value: password }, }); } if (displayName !== undefined) { await ocsRequest(s.ncUser, s.ncPass, 'PUT', `/ocs/v1.php/cloud/users/${encodeURIComponent(id)}`, { body: { key: 'displayname', value: displayName }, }); } if (email !== undefined) { await ocsRequest(s.ncUser, s.ncPass, 'PUT', `/ocs/v1.php/cloud/users/${encodeURIComponent(id)}`, { body: { key: 'email', value: email }, }); } if (enabled === false) { await ocsRequest(s.ncUser, s.ncPass, 'PUT', `/ocs/v1.php/cloud/users/${encodeURIComponent(id)}/disable`); } if (enabled === true) { await ocsRequest(s.ncUser, s.ncPass, 'PUT', `/ocs/v1.php/cloud/users/${encodeURIComponent(id)}/enable`); } sendJson(res, 200, { ok: true }); } catch (e) { sendJson(res, 400, { error: e.message }); } }, }, { method: 'DELETE', path: '/api/users/:id', handler: async (req, res, params) => { const s = getSession(req); if (!s?.ncUser) return sendJson(res, 401, { error: 'Требуется вход' }); try { await ocsRequest(s.ncUser, s.ncPass, 'DELETE', `/ocs/v1.php/cloud/users/${encodeURIComponent(params.id)}`); sendJson(res, 200, { ok: true }); } catch (e) { sendJson(res, 400, { error: e.message }); } }, }, { method: 'POST', path: '/api/users/:id/groups', handler: async (req, res, params) => { const s = getSession(req); if (!s?.ncUser) return sendJson(res, 401, { error: 'Требуется вход' }); const { groupid } = await readJsonBody(req); if (!groupid) return sendJson(res, 400, { error: 'groupid обязателен' }); try { await ocsRequest(s.ncUser, s.ncPass, 'POST', `/ocs/v1.php/cloud/users/${encodeURIComponent(params.id)}/groups`, { body: { groupid } }); sendJson(res, 200, { ok: true }); } catch (e) { sendJson(res, 400, { error: e.message }); } }, }, { method: 'DELETE', path: '/api/users/:id/groups/:groupid', handler: async (req, res, params) => { const s = getSession(req); if (!s?.ncUser) return sendJson(res, 401, { error: 'Требуется вход' }); try { await ocsRequest(s.ncUser, s.ncPass, 'DELETE', `/ocs/v1.php/cloud/users/${encodeURIComponent(params.id)}/groups/${encodeURIComponent(params.groupid)}`); sendJson(res, 200, { ok: true }); } catch (e) { sendJson(res, 400, { error: e.message }); } }, }, ]; async function handleRequest(req, res) { const ip = clientIp(req); if (ALLOWED_IPS.length && !ALLOWED_IPS.includes(ip) && !ALLOWED_IPS.includes(req.socket?.remoteAddress)) { return sendJson(res, 403, { error: 'Доступ запрещён с этого IP' }); } const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`); const matched = matchRoute(req.method, url.pathname, routes); if (matched) { return matched.handler(req, res, matched.params); } const file = await serveStatic(PUBLIC_DIR, url.pathname); if (file) { res.writeHead(200, { 'Content-Type': file.type, 'Content-Length': file.body.length }); res.end(file.body); return; } sendJson(res, 404, { error: 'Not found' }); } listen(handleRequest, { host: HOST, port: PORT, http2: USE_HTTP2, tls: TLS, }); console.log(`Nextcloud API → ${NEXTCLOUD_URL}`);