306 lines
11 KiB
JavaScript
306 lines
11 KiB
JavaScript
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}`);
|