Files
puter/src/backend/controllers/system/SystemController.js
T
0801e1dc44 feat: add signup disable config and fix telemetry startup (#3319)
* feat: add signup disable config and fix telemetry startup

* fix: nits for config spread and 403 checks

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:52:29 -07:00

174 lines
6.5 KiB
JavaScript

/**
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { HttpError } from '../../core/http/HttpError.js';
import { PuterController } from '../types.js';
/**
* System-level endpoints — health, version, contact.
*
* These are all low-risk, authenticated or not, and mostly stateless.
*/
export class SystemController extends PuterController {
constructor(config, clients, stores, services, drivers) {
super(config, clients, stores, services, drivers);
this.bootTime = Date.now();
}
registerRoutes(
/** @type {import('../../core/http/PuterRouter.js').PuterRouter} */
router,
) {
// -- Healthcheck ---------------------------------------------
// Delegates to ServerHealthService for the real check-based
// status. Returns `{ ok: true }` when all registered checks pass,
// or `{ ok: false, failed: [...] }` + 503 when any fail or the
// server is draining.
router.get('/healthcheck', { subdomain: '*' }, async (_req, res) => {
const health = this.services.health;
if (!health || typeof health.getStatus !== 'function') {
// Fallback for boot ordering / missing service.
return res.send('ok');
}
const status = await health.getStatus();
if (!status.ok) return res.status(503).json(status);
return res.json(status);
});
// -- Version -------------------------------------------------
router.get('/version', { subdomain: '*' }, (_req, res) => {
const version =
this.config.version ??
process.env.npm_package_version ??
'unknown';
const parts = String(version).split('.');
res.json({
version,
major: parts[0] ? Number(parts[0]) : null,
minor: parts[1] ? Number(parts[1]) : null,
patch: parts[2] ? Number(parts[2]) : null,
environment: this.config.env ?? 'prod',
location: this.config.serverId ?? null,
deploy_timestamp: this.bootTime,
});
});
// -- Contact us ----------------------------------------------
router.post(
'/contactUs',
{
subdomain: 'api',
requireUserActor: true,
allowFullAccessToken: true,
rateLimit: {
scope: 'contact-us',
limit: 10,
window: 15 * 60_000,
key: 'user',
},
},
async (req, res) => {
const { message } = req.body ?? {};
if (!message || typeof message !== 'string') {
throw new HttpError(400, '`message` is required', {
legacyCode: 'bad_request',
});
}
if (message.length > 100_000) {
throw new HttpError(
400,
'`message` is too long (max 100,000 characters)',
{ legacyCode: 'bad_request' },
);
}
// Persist to feedback table for durability
try {
await this.clients.db.write(
'INSERT INTO `feedback` (`user_id`, `message`) VALUES (?, ?)',
[req.actor.user.id, message],
);
} catch (e) {
console.warn('[contactUs] feedback insert failed:', e);
}
// Send to support email
const supportEmail =
this.config.support_email ?? 'support@puter.com';
if (this.clients.email && req.actor.user?.email) {
try {
await this.clients.email.sendRaw({
to: supportEmail,
replyTo: req.actor.user.email,
subject: `Contact from ${req.actor.user.username}`,
text: message,
});
} catch (e) {
console.warn('[contactUs] email send failed:', e);
}
}
res.json({});
},
);
// -- GET /whoarewe -------------------------------------------
router.get('/whoarewe', {}, (_req, res) => {
res.json({
name: 'Puter',
version: this.config.version ?? null,
environment: this.config.env ?? 'prod',
disable_user_signup: Boolean(this.config.disable_user_signup),
});
});
// -- GET /lsmod ----------------------------------------------
// Enumerates driver interfaces and their implementors.
router.get(
'/lsmod',
{ subdomain: 'api', requireAuth: true },
(_req, res) => {
const interfaces = {};
for (const [key, driver] of Object.entries(this.drivers)) {
const ifaceName = driver?.driverInterface;
if (!ifaceName) continue;
const driverName = driver.driverName ?? key;
if (!interfaces[ifaceName]) {
interfaces[ifaceName] = { implementors: {} };
}
interfaces[ifaceName].implementors[driverName] = {
isDefault: Boolean(driver.isDefault),
};
}
res.json({ interfaces });
},
);
}
onServerStart() {}
onServerPrepareShutdown() {
globalThis.__puter_draining = true;
}
onServerShutdown() {}
}