/*
* 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 .
*/
import { PuterController } from '../types.js';
import type { PuterRouter } from '../../core/http/PuterRouter';
import { HttpError } from '../../core/http/HttpError.js';
/**
* One-off user-facing pages.
*
* /robots.txt — static text /sitemap.xml — docs + approved apps /unsubscribe —
* toggles `user.unsubscribed` from an email link /confirm-email-by-token —
* email-link confirmation flow (distinct from the POST /confirm-email JSON API
* used by the in-app code-entry form)
*
* All root-subdomain-only, all unauthenticated (the confirm/unsubscribe tokens
* in the query string are the auth).
*/
/**
* Unauthenticated pages reached from a token-bearing email link. Matches what
* the emailSend extension already applies to its own link pages.
*/
const TOKEN_LINK_LIMIT = {
scope: 'token-link-page',
limit: 30,
window: 60_000,
key: 'ip' as const,
};
export class StaticPagesController extends PuterController {
registerRoutes(router: PuterRouter) {
const origin = this.config.origin ?? '';
const docsOrigin = (() => {
const d = this.config.domain;
return d ? `https://docs.${d}` : '';
})();
const page = (
icon: string,
title: string,
msg: string,
color: string,
) => `
${title} — Puter
`;
const err = (msg: string) =>
page('✕', 'Something went wrong', msg, '#e53e3e');
const ok = (msg: string) => page('✓', 'Success', msg, '#38a169');
// -- /robots.txt ---------------------------------------------
router.get('/robots.txt', {}, (req, res) => {
const domain = this.config.domain ?? req.hostname;
const disallowed = [
'AhrefsBot',
'BLEXBot',
'DotBot',
'ia_archiver',
'MJ12bot',
'SearchmetricsBot',
'SemrushBot',
];
const body =
disallowed
.map((ua) => `User-agent: ${ua}\nDisallow: /\n`)
.join('\n') +
`\nSitemap: ${req.protocol}://${domain}/sitemap.xml\n`;
res.type('text/plain').send(body);
});
// -- /sitemap.xml --------------------------------------------
router.get(
'/sitemap.xml',
{
// Unauthenticated and runs a full-table scan over approved
// apps on every request, with no response cache in front.
rateLimit: {
scope: 'sitemap',
limit: 10,
window: 60_000,
key: 'ip',
},
},
async (req, res) => {
const domain = this.config.domain ?? req.hostname;
const origin = `${req.protocol}://${domain}`;
const apps = (await this.clients.db.read(
`SELECT \`name\` FROM \`apps\` WHERE \`approved_for_listing\` = ${this.clients.db.booleanLiteral(true)}`,
)) as Array<{ name: string }>;
const urls = [
`${req.protocol}://docs.${domain}/`,
...apps.map(
(a) => `${origin}/app/${a.name}`,
),
];
const body =
'' +
'' +
urls.join('') +
'';
res.type('application/xml').send(body);
},
);
// -- /unsubscribe --------------------------------------------
router.get(
'/unsubscribe',
{ rateLimit: TOKEN_LINK_LIMIT },
async (req, res) => {
const userUuid =
typeof req.query.user_uuid === 'string'
? req.query.user_uuid
: undefined;
if (!userUuid) {
res.send(err('user_uuid is required'));
return;
}
const user = await this.stores.user.getByUuid(userUuid);
if (!user) {
res.send(err('User not found.'));
return;
}
if (user.unsubscribed) {
res.send(ok('You are already unsubscribed.'));
return;
}
await this.stores.user.update(user.id, { unsubscribed: 1 });
res.send(
ok('You have successfully unsubscribed from all emails.'),
);
},
);
// -- /confirm-email-by-token ---------------------------------
router.get(
'/confirm-email-by-token',
{ rateLimit: TOKEN_LINK_LIMIT },
async (req, res) => {
const userUuid =
typeof req.query.user_uuid === 'string'
? req.query.user_uuid
: undefined;
const token =
typeof req.query.token === 'string'
? req.query.token
: undefined;
if (!userUuid) {
res.send(err('user_uuid is required'));
return;
}
if (!token) {
res.send(err('token is required'));
return;
}
const user = await this.stores.user.getByProperty(
'uuid',
userUuid,
{ force: true },
);
if (!user) {
res.send(err('user not found.'));
return;
}
if (user.email_confirmed) {
res.send(ok('Email already confirmed.'));
return;
}
if (user.email_confirm_token !== token) {
res.send(err('invalid token.'));
return;
}
try {
await this.services.signup.confirmEmail(user);
} catch (e) {
if (!(e instanceof HttpError)) throw e;
res.send(err(e.message));
return;
}
res.send(ok('Your email has been successfully confirmed.'));
},
);
}
}