181 lines
5.8 KiB
JavaScript
181 lines
5.8 KiB
JavaScript
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'}`);
|