/* * 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
${icon}

${title}

${msg}

`; 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.')); }, ); } }