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

181 lines
5.8 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,
clientIp,
matchRoute,
serveStatic,
listen,
createRateLimiter,
dirFromMeta,
} from '../lib/node-server.js';
const __dirname = dirFromMeta(import.meta.url);
loadEnv(path.join(__dirname, '.env'));
const PORT = Number(process.env.REGISTER_UI_PORT || 3001);
const HOST = process.env.REGISTER_UI_HOST || '0.0.0.0';
const USE_HTTP2 = process.env.HTTP2 === 'true';
const NEXTCLOUD_URL = (process.env.NEXTCLOUD_URL || 'http://127.0.0.1:8080').replace(/\/$/, '');
const ADMIN_USER = process.env.REGISTRATION_ADMIN_USER || '';
const ADMIN_PASS = process.env.REGISTRATION_ADMIN_PASSWORD || '';
const DEFAULT_QUOTA = process.env.DEFAULT_QUOTA || '5GB';
const DEFAULT_GROUP = process.env.DEFAULT_GROUP || 'users';
const ENABLED = process.env.REGISTRATION_ENABLED !== 'false';
const PUBLIC_DIR = path.join(__dirname, 'public');
const ALLOWED_DOMAINS = (process.env.ALLOWED_EMAIL_DOMAINS || '')
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
const TLS =
process.env.SSL_KEY && process.env.SSL_CERT
? { key: process.env.SSL_KEY, cert: process.env.SSL_CERT }
: null;
const rateLimit = createRateLimiter({
windowMs: 3600_000,
max: Number(process.env.RATE_LIMIT_PER_HOUR || 10),
});
function authHeader() {
return 'Basic ' + Buffer.from(`${ADMIN_USER}:${ADMIN_PASS}`).toString('base64');
}
async function ocs(method, ocsPath, body) {
const headers = {
'OCS-APIRequest': 'true',
Accept: 'application/json',
Authorization: authHeader(),
};
const init = { method, headers };
if (body) {
headers['Content-Type'] = 'application/x-www-form-urlencoded';
init.body = new URLSearchParams(body).toString();
}
const res = await fetch(`${NEXTCLOUD_URL}${ocsPath}`, init);
const data = await res.json().catch(() => ({}));
const meta = data?.ocs?.meta;
if (!meta || meta.status !== 'ok') throw new Error(meta?.message || 'Ошибка Nextcloud');
return data.ocs.data;
}
function validateUsername(id) {
return /^[a-zA-Z0-9._@-]{3,64}$/.test(id);
}
function validatePassword(p) {
return typeof p === 'string' && p.length >= 8;
}
function validateEmail(email) {
if (!email) return true;
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return false;
if (ALLOWED_DOMAINS.length) {
return ALLOWED_DOMAINS.includes(email.split('@')[1]?.toLowerCase());
}
return true;
}
const routes = [
{
method: 'GET',
path: '/api/config',
handler: async (_req, res) => {
sendJson(res, 200, {
enabled: ENABLED && Boolean(ADMIN_USER && ADMIN_PASS),
nextcloudUrl: NEXTCLOUD_URL,
defaultQuota: DEFAULT_QUOTA,
allowedEmailDomains: ALLOWED_DOMAINS,
});
},
},
{
method: 'POST',
path: '/api/register',
handler: async (req, res) => {
if (!rateLimit(clientIp(req))) {
return sendJson(res, 429, { error: 'Слишком много попыток. Попробуйте позже.' });
}
if (!ENABLED) return sendJson(res, 403, { error: 'Регистрация временно отключена' });
if (!ADMIN_USER || !ADMIN_PASS) {
return sendJson(res, 503, { error: 'Сервер не настроен (REGISTRATION_ADMIN_* в .env)' });
}
const { username, password, email, displayName } = await readJsonBody(req);
if (!validateUsername(username)) {
return sendJson(res, 400, { error: 'Логин: 3–64 символа, латиница, цифры, . _ @ -' });
}
if (!validatePassword(password)) {
return sendJson(res, 400, { error: 'Пароль минимум 8 символов' });
}
if (!validateEmail(email || '')) {
return sendJson(res, 400, {
error: ALLOWED_DOMAINS.length
? `Разрешены только email: ${ALLOWED_DOMAINS.join(', ')}`
: 'Некорректный email',
});
}
try {
await ocs('POST', '/ocs/v1.php/cloud/users', { userid: username, password });
if (DEFAULT_QUOTA) {
await ocs('PUT', `/ocs/v1.php/cloud/users/${encodeURIComponent(username)}`, {
key: 'quota', value: DEFAULT_QUOTA,
});
}
if (email) {
await ocs('PUT', `/ocs/v1.php/cloud/users/${encodeURIComponent(username)}`, {
key: 'email', value: email,
});
}
if (displayName) {
await ocs('PUT', `/ocs/v1.php/cloud/users/${encodeURIComponent(username)}`, {
key: 'displayname', value: displayName,
});
}
if (DEFAULT_GROUP) {
await ocs('POST', `/ocs/v1.php/cloud/users/${encodeURIComponent(username)}/groups`, {
groupid: DEFAULT_GROUP,
});
}
sendJson(res, 200, { ok: true, username, loginUrl: NEXTCLOUD_URL });
} catch (e) {
const msg = e.message || 'Ошибка регистрации';
if (/already exists|102/i.test(msg)) {
return sendJson(res, 409, { error: 'Пользователь с таким логином уже существует' });
}
sendJson(res, 400, { error: msg });
}
},
},
];
async function handleRequest(req, res) {
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 → ${NEXTCLOUD_URL}`);
console.log(`Registration → ${ENABLED ? 'enabled' : 'disabled'}`);