Enforce ownership of puter.site subdomains for apps

This commit is contained in:
jelveh
2025-11-25 15:43:14 -08:00
parent 6558bc26e9
commit 014b5ac713
2 changed files with 57 additions and 8 deletions
+4
View File
@@ -323,6 +323,10 @@ module.exports = class APIError {
status: 400,
message: ({ subdomain }) => `Subdomain ${quot(subdomain)} is not available.`,
},
'subdomain_not_owned': {
status: 403,
message: ({ subdomain }) => `You must own the ${quot(subdomain)} subdomain on Puter to use it for this app.`,
},
// Users
'email_already_in_use': {
+53 -8
View File
@@ -17,6 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const APIError = require('../../api/APIError');
const config = require('../../config');
const { app_name_exists, refresh_apps_cache } = require('../../helpers');
const { AppUnderUserActorType } = require('../../services/auth/Actor');
@@ -103,6 +104,16 @@ class AppES extends BaseES {
* @returns {Promise<Object>} Upsert operation results
*/
async upsert (entity, extra) {
const actor = Context.get('actor');
const user = actor?.type?.user;
const full_entity = extra.old_entity
? await (await extra.old_entity.clone()).apply(entity)
: entity
;
await this.ensure_puter_site_subdomain_is_owned_(full_entity, extra, user);
if ( await app_name_exists(await entity.get('name')) ) {
const { old_entity } = extra;
const is_name_change = ( !old_entity ) ||
@@ -198,13 +209,6 @@ class AppES extends BaseES {
: await entity.get('owner');
{
const { old_entity } = extra;
const full_entity = old_entity
? await (await old_entity.clone()).apply(entity)
: entity
;
// Update app cache
const raw_app = {
// These map to different names
@@ -353,7 +357,48 @@ class AppES extends BaseES {
return subdomain_id;
},
/**
* Ensures that when an app uses a puter.site subdomain as its index_url,
* the subdomain belongs to the user creating/updating the app.
*/
async ensure_puter_site_subdomain_is_owned_ (entity, extra, user) {
if ( ! user ) return;
// Only enforce when the index_url is being set or changed
const new_index_url = await entity.get('index_url');
if ( ! new_index_url ) return;
if ( extra.old_entity ) {
const old_index_url = await extra.old_entity.get('index_url');
if ( old_index_url === new_index_url ) {
return;
}
}
let hostname;
try {
hostname = (new URL(new_index_url)).hostname.toLowerCase();
} catch {
return;
}
const hosting_domain = config.static_hosting_domain?.toLowerCase();
if ( ! hosting_domain ) return;
const suffix = `.${hosting_domain}`;
if ( ! hostname.endsWith(suffix) ) return;
const subdomain = hostname.slice(0, hostname.length - suffix.length);
if ( ! subdomain ) return;
const svc_puterSite = this.context.get('services').get('puter-site');
const site = await svc_puterSite.get_subdomain(subdomain, { is_custom_domain: false });
if ( ! site || site.user_id !== user.id ) {
throw APIError.create('subdomain_not_owned', null, { subdomain });
}
},
};
}
module.exports = AppES;
module.exports = AppES;