nextcloud-install/admin-ui/server.js
2026-06-24 09:23:58 +03:00

306 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}`);