mirror of
https://github.com/pockethost/pockethost.git
synced 2025-11-24 14:35:49 +00:00
Merge branch 'main' of github.com:pockethost/pockethost
This commit is contained in:
commit
2624833221
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CodeSample from '$components/CodeSample.svelte'
|
import CodeSample from '$components/CodeSample.svelte'
|
||||||
import CardHeader from '$src/components/cards/CardHeader.svelte'
|
import CardHeader from '$src/components/cards/CardHeader.svelte'
|
||||||
import { DISCORD_URL, INSTANCE_URL } from '$src/env'
|
import { INSTANCE_URL } from '$src/env'
|
||||||
import { isCnameActive } from 'pockethost/common'
|
|
||||||
import { instance } from './store'
|
import { instance } from './store'
|
||||||
|
|
||||||
let installSnippet = `npm i pocketbase`
|
let installSnippet = `npm i pocketbase`
|
||||||
|
|||||||
@ -92,15 +92,16 @@ export const init = () => {
|
|||||||
|
|
||||||
onAuthChange((authStoreProps) => {
|
onAuthChange((authStoreProps) => {
|
||||||
const isLoggedIn = authStoreProps.isValid
|
const isLoggedIn = authStoreProps.isValid
|
||||||
isUserLoggedIn.set(isLoggedIn)
|
console.log(`onAuthChange update`, { isLoggedIn, authStoreProps })
|
||||||
const user = authStoreProps.model as UserFields
|
const user = authStoreProps.model as UserFields
|
||||||
userStore.set(isLoggedIn ? user : undefined)
|
userStore.set(isLoggedIn ? user : undefined)
|
||||||
isAuthStateInitialized.set(true)
|
isAuthStateInitialized.set(true)
|
||||||
|
isUserLoggedIn.set(isLoggedIn)
|
||||||
tryUserSubscribe(user?.id)
|
tryUserSubscribe(user?.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
userStore.subscribe((user) => {
|
userStore.subscribe((user) => {
|
||||||
console.log(`userStore.subscribe`, { user })
|
console.log(`userStore.subscribe update`, { user })
|
||||||
const isPaid = [SubscriptionType.Founder, SubscriptionType.Premium, SubscriptionType.Flounder].includes(
|
const isPaid = [SubscriptionType.Founder, SubscriptionType.Premium, SubscriptionType.Flounder].includes(
|
||||||
user?.subscription || SubscriptionType.Free
|
user?.subscription || SubscriptionType.Free
|
||||||
)
|
)
|
||||||
@ -113,22 +114,25 @@ export const init = () => {
|
|||||||
// This holds an array of all the user's instances and their data
|
// This holds an array of all the user's instances and their data
|
||||||
|
|
||||||
/** Listen for instances */
|
/** Listen for instances */
|
||||||
|
let unsubInstanceWatch: UnsubscribeFunc | undefined
|
||||||
isUserLoggedIn.subscribe(async (isLoggedIn) => {
|
isUserLoggedIn.subscribe(async (isLoggedIn) => {
|
||||||
let unsub: UnsubscribeFunc | undefined
|
console.log(`isUserLoggedIn.subscribe update`, { isLoggedIn })
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
userStore.set(undefined)
|
userStore.set(undefined)
|
||||||
globalInstancesStore.set({})
|
globalInstancesStore.set({})
|
||||||
globalInstancesStoreReady.set(false)
|
globalInstancesStoreReady.set(false)
|
||||||
unsub?.()
|
unsubInstanceWatch?.()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
unsub = undefined
|
unsubInstanceWatch = undefined
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { getAllInstancesById } = client()
|
const { getAllInstancesById } = client()
|
||||||
|
|
||||||
|
console.log('Getting all instances by ID')
|
||||||
const instances = await getAllInstancesById()
|
const instances = await getAllInstancesById()
|
||||||
|
console.log('Instances', instances)
|
||||||
|
|
||||||
globalInstancesStore.set(instances)
|
globalInstancesStore.set(instances)
|
||||||
globalInstancesStoreReady.set(true)
|
globalInstancesStoreReady.set(true)
|
||||||
@ -137,13 +141,14 @@ export const init = () => {
|
|||||||
client()
|
client()
|
||||||
.client.collection('instances')
|
.client.collection('instances')
|
||||||
.subscribe<InstanceFields>('*', (data) => {
|
.subscribe<InstanceFields>('*', (data) => {
|
||||||
|
console.log('Instance subscribe update', data)
|
||||||
globalInstancesStore.update((instances) => {
|
globalInstancesStore.update((instances) => {
|
||||||
instances[data.record.id] = data.record
|
instances[data.record.id] = data.record
|
||||||
return instances
|
return instances
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then((u) => {
|
.then((u) => {
|
||||||
unsub = u
|
unsubInstanceWatch = u
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.error('Failed to subscribe to instances')
|
console.error('Failed to subscribe to instances')
|
||||||
@ -171,6 +176,7 @@ const tryUserSubscribe = (() => {
|
|||||||
client().client.collection('users').authRefresh().catch(console.error)
|
client().client.collection('users').authRefresh().catch(console.error)
|
||||||
})
|
})
|
||||||
.then((u) => {
|
.then((u) => {
|
||||||
|
console.log('Subscribed to user', id)
|
||||||
unsub = async () => {
|
unsub = async () => {
|
||||||
console.log('Unsubscribing from user', id)
|
console.log('Unsubscribing from user', id)
|
||||||
await u()
|
await u()
|
||||||
|
|||||||
@ -42,9 +42,9 @@ export const firewall = async () => {
|
|||||||
app.use(cors())
|
app.use(cors())
|
||||||
app.use(enforce.HTTPS())
|
app.use(enforce.HTTPS())
|
||||||
|
|
||||||
app.get(`/_api/firewall/health`, (req, res, next) => {
|
app.get(`/api/firewall/health`, (req, res, next) => {
|
||||||
dbg(`Health check`)
|
dbg(`Health check`)
|
||||||
res.json({ status: 'firewall ok' })
|
res.json({ status: 'firewall ok', code: 200 })
|
||||||
res.end()
|
res.end()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -91,7 +91,7 @@ const HandleInstanceDelete = (c) => {
|
|||||||
const HandleInstanceResolve = (c) => {
|
const HandleInstanceResolve = (c) => {
|
||||||
const dao = $app.dao();
|
const dao = $app.dao();
|
||||||
const log = mkLog(`GET:instance/resolve`);
|
const log = mkLog(`GET:instance/resolve`);
|
||||||
log(`***TOP OF GET`);
|
log(`TOP OF GET`);
|
||||||
const host = c.queryParam("host");
|
const host = c.queryParam("host");
|
||||||
if (!host) throw new BadRequestError(`Host is required when resolving an instance.`);
|
if (!host) throw new BadRequestError(`Host is required when resolving an instance.`);
|
||||||
const instance = (() => {
|
const instance = (() => {
|
||||||
@ -203,6 +203,43 @@ const removeEmptyKeys = (obj) => {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region src/lib/handlers/instance/api/HandleInstanceUpdate.ts
|
//#region src/lib/handlers/instance/api/HandleInstanceUpdate.ts
|
||||||
|
const callCloudflareAPI = (endpoint, method, body, log) => {
|
||||||
|
const apiToken = $os.getenv("MOTHERSHIP_CLOUDFLARE_API_TOKEN");
|
||||||
|
const zoneId = $os.getenv("MOTHERSHIP_CLOUDFLARE_ZONE_ID");
|
||||||
|
if (!apiToken || !zoneId) {
|
||||||
|
if (log) log("Cloudflare API credentials not configured - skipping Cloudflare operations");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/${endpoint}`;
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
timeout: 30
|
||||||
|
};
|
||||||
|
if (body) config.body = JSON.stringify(body);
|
||||||
|
if (log) log(`Making Cloudflare API call: ${method} ${url}`, config);
|
||||||
|
const response = $http.send(config);
|
||||||
|
if (log) log(`Cloudflare API response:`, response);
|
||||||
|
return response;
|
||||||
|
} catch (error$1) {
|
||||||
|
if (log) log(`Cloudflare API error:`, error$1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createCloudflareCustomHostname = (hostname, log) => {
|
||||||
|
return callCloudflareAPI("custom_hostnames", "POST", {
|
||||||
|
hostname,
|
||||||
|
ssl: {
|
||||||
|
method: "http",
|
||||||
|
type: "dv"
|
||||||
|
}
|
||||||
|
}, log);
|
||||||
|
};
|
||||||
const HandleInstanceUpdate = (c) => {
|
const HandleInstanceUpdate = (c) => {
|
||||||
const dao = $app.dao();
|
const dao = $app.dao();
|
||||||
const log = mkLog(`PUT:instance`);
|
const log = mkLog(`PUT:instance`);
|
||||||
@ -239,6 +276,14 @@ const HandleInstanceUpdate = (c) => {
|
|||||||
log(`authRecord`, JSON.stringify(authRecord));
|
log(`authRecord`, JSON.stringify(authRecord));
|
||||||
if (!authRecord) throw new Error(`Expected authRecord here`);
|
if (!authRecord) throw new Error(`Expected authRecord here`);
|
||||||
if (record.get("uid") !== authRecord.id) throw new BadRequestError(`Not authorized`);
|
if (record.get("uid") !== authRecord.id) throw new BadRequestError(`Not authorized`);
|
||||||
|
const oldCname = record.getString("cname").trim();
|
||||||
|
const newCname = cname ? cname.trim() : "";
|
||||||
|
const cnameChanged = oldCname !== newCname;
|
||||||
|
if (cnameChanged && newCname) {
|
||||||
|
log(`CNAME changed from "${oldCname}" to "${newCname}" - adding to Cloudflare`);
|
||||||
|
const createResponse = createCloudflareCustomHostname(newCname, log);
|
||||||
|
if (createResponse) log(`Cloudflare API call completed for "${newCname}" - frontend will poll for health`);
|
||||||
|
}
|
||||||
const sanitized = removeEmptyKeys({
|
const sanitized = removeEmptyKeys({
|
||||||
subdomain,
|
subdomain,
|
||||||
version,
|
version,
|
||||||
@ -385,45 +430,6 @@ const HandleMigrateRegions = (e) => {
|
|||||||
log(`Migrated regions`);
|
log(`Migrated regions`);
|
||||||
};
|
};
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region src/lib/handlers/instance/model/HandleInstanceBeforeUpdate.ts
|
|
||||||
const HandleInstanceBeforeUpdate = (e) => {
|
|
||||||
const dao = e.dao || $app.dao();
|
|
||||||
const log = mkLog(`instances-validate-before-update`);
|
|
||||||
const id = e.model.getId();
|
|
||||||
const version = e.model.get("version");
|
|
||||||
if (!versions.includes(version)) {
|
|
||||||
const msg = `[ERROR] Invalid version '${version}' for [${id}]. Version must be one of: ${versions.join(", ")}`;
|
|
||||||
log(`${msg}`);
|
|
||||||
throw new BadRequestError(msg);
|
|
||||||
}
|
|
||||||
const cname = e.model.get("cname");
|
|
||||||
if (cname.length > 0) {
|
|
||||||
const result = new DynamicModel({ id: "" });
|
|
||||||
const inUse = (() => {
|
|
||||||
try {
|
|
||||||
dao.db().newQuery(`select id from instances where cname='${cname}' and id <> '${id}'`).one(result);
|
|
||||||
} catch (e$1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})();
|
|
||||||
if (inUse) {
|
|
||||||
const msg = `[ERROR] [${id}] Custom domain ${cname} already in use.`;
|
|
||||||
log(`${msg}`);
|
|
||||||
throw new BadRequestError(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
//#region src/lib/handlers/instance/model/HandleInstanceVersionValidation.ts
|
|
||||||
const HandleInstanceVersionValidation = (e) => {
|
|
||||||
const dao = e.dao || $app.dao();
|
|
||||||
const version = e.model.get("version");
|
|
||||||
if (!versions.includes(version)) throw new BadRequestError(`Invalid version ${version}. Version must be one of: ${versions.join(", ")}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region src/lib/util/mkAudit.ts
|
//#region src/lib/util/mkAudit.ts
|
||||||
const mkAudit = (log, dao) => {
|
const mkAudit = (log, dao) => {
|
||||||
@ -439,8 +445,8 @@ const mkAudit = (log, dao) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region src/lib/handlers/instance/model/HandleNotifyDiscordAfterCreate.ts
|
//#region src/lib/handlers/instance/model/AfterCreate_notify_discord.ts
|
||||||
const HandleNotifyDiscordAfterCreate = (e) => {
|
const AfterCreate_notify_discord = (e) => {
|
||||||
const dao = e.dao || $app.dao();
|
const dao = e.dao || $app.dao();
|
||||||
const log = mkLog(`instances:create:discord:notify`);
|
const log = mkLog(`instances:create:discord:notify`);
|
||||||
const audit = mkAudit(log, dao);
|
const audit = mkAudit(log, dao);
|
||||||
@ -460,6 +466,46 @@ const HandleNotifyDiscordAfterCreate = (e) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region src/lib/handlers/instance/model/BeforeUpdate_cname.ts
|
||||||
|
const BeforeUpdate_cname = (e) => {
|
||||||
|
const dao = e.dao || $app.dao();
|
||||||
|
const log = mkLog(`BeforeUpdate_cname`);
|
||||||
|
const id = e.model.getId();
|
||||||
|
const newCname = e.model.get("cname").trim();
|
||||||
|
if (newCname.length > 0) {
|
||||||
|
const result = new DynamicModel({ id: "" });
|
||||||
|
const inUse = (() => {
|
||||||
|
try {
|
||||||
|
dao.db().newQuery(`select id from instances where cname='${newCname}' and id <> '${id}'`).one(result);
|
||||||
|
} catch (e$1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})();
|
||||||
|
if (inUse) {
|
||||||
|
const msg = `[ERROR] [${id}] Custom domain ${newCname} already in use.`;
|
||||||
|
log(`${msg}`);
|
||||||
|
throw new BadRequestError(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log(`CNAME validation passed for: "${newCname}"`);
|
||||||
|
};
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region src/lib/handlers/instance/model/BeforeUpdate_version.ts
|
||||||
|
const BeforeUpdate_version = (e) => {
|
||||||
|
const dao = e.dao || $app.dao();
|
||||||
|
const log = mkLog(`BeforeUpdate_version`);
|
||||||
|
const version = e.model.get("version");
|
||||||
|
log(`Validating version ${version}`);
|
||||||
|
if (!versions.includes(version)) {
|
||||||
|
const msg = `Invalid version ${version}. Version must be one of: ${versions.join(", ")}`;
|
||||||
|
log(`[ERROR] ${msg}`);
|
||||||
|
throw new BadRequestError(msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region src/lib/util/mkNotifier.ts
|
//#region src/lib/util/mkNotifier.ts
|
||||||
const mkNotifier = (log, dao) => (channel, template, user_id, context = {}) => {
|
const mkNotifier = (log, dao) => (channel, template, user_id, context = {}) => {
|
||||||
@ -3088,12 +3134,13 @@ const HandleVersionsRequest = (c) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
exports.HandleInstanceBeforeUpdate = HandleInstanceBeforeUpdate;
|
exports.AfterCreate_notify_discord = AfterCreate_notify_discord;
|
||||||
|
exports.BeforeUpdate_cname = BeforeUpdate_cname;
|
||||||
|
exports.BeforeUpdate_version = BeforeUpdate_version;
|
||||||
exports.HandleInstanceCreate = HandleInstanceCreate;
|
exports.HandleInstanceCreate = HandleInstanceCreate;
|
||||||
exports.HandleInstanceDelete = HandleInstanceDelete;
|
exports.HandleInstanceDelete = HandleInstanceDelete;
|
||||||
exports.HandleInstanceResolve = HandleInstanceResolve;
|
exports.HandleInstanceResolve = HandleInstanceResolve;
|
||||||
exports.HandleInstanceUpdate = HandleInstanceUpdate;
|
exports.HandleInstanceUpdate = HandleInstanceUpdate;
|
||||||
exports.HandleInstanceVersionValidation = HandleInstanceVersionValidation;
|
|
||||||
exports.HandleInstancesResetIdle = HandleInstancesResetIdle;
|
exports.HandleInstancesResetIdle = HandleInstancesResetIdle;
|
||||||
exports.HandleLemonSqueezySale = HandleLemonSqueezySale;
|
exports.HandleLemonSqueezySale = HandleLemonSqueezySale;
|
||||||
exports.HandleMailSend = HandleMailSend;
|
exports.HandleMailSend = HandleMailSend;
|
||||||
@ -3102,7 +3149,6 @@ exports.HandleMigrateCnamesToDomains = HandleMigrateCnamesToDomains;
|
|||||||
exports.HandleMigrateInstanceVersions = HandleMigrateInstanceVersions;
|
exports.HandleMigrateInstanceVersions = HandleMigrateInstanceVersions;
|
||||||
exports.HandleMigrateRegions = HandleMigrateRegions;
|
exports.HandleMigrateRegions = HandleMigrateRegions;
|
||||||
exports.HandleMirrorData = HandleMirrorData;
|
exports.HandleMirrorData = HandleMirrorData;
|
||||||
exports.HandleNotifyDiscordAfterCreate = HandleNotifyDiscordAfterCreate;
|
|
||||||
exports.HandleProcessNotification = HandleProcessNotification;
|
exports.HandleProcessNotification = HandleProcessNotification;
|
||||||
exports.HandleProcessSingleNotification = HandleProcessSingleNotification;
|
exports.HandleProcessSingleNotification = HandleProcessSingleNotification;
|
||||||
exports.HandleSesError = HandleSesError;
|
exports.HandleSesError = HandleSesError;
|
||||||
|
|||||||
@ -13,24 +13,20 @@ routerAdd("GET", "/api/instance/resolve", (c) => {
|
|||||||
return require(`${__hooks}/mothership`).HandleInstanceResolve(c);
|
return require(`${__hooks}/mothership`).HandleInstanceResolve(c);
|
||||||
}, $apis.requireAdminAuth());
|
}, $apis.requireAdminAuth());
|
||||||
/** Validate instance version */
|
/** Validate instance version */
|
||||||
onModelBeforeCreate((e) => {
|
onModelBeforeUpdate((e) => {
|
||||||
return require(`${__hooks}/mothership`).HandleInstanceVersionValidation(e);
|
return require(`${__hooks}/mothership`).BeforeUpdate_version(e);
|
||||||
}, "instances");
|
}, "instances");
|
||||||
onModelAfterCreate((e) => {}, "instances");
|
/** Validate cname */
|
||||||
|
onModelBeforeUpdate((e) => {
|
||||||
|
return require(`${__hooks}/mothership`).BeforeUpdate_cname(e);
|
||||||
|
}, "instances");
|
||||||
|
/** Notify discord on instance create */
|
||||||
onAfterBootstrap((e) => {});
|
onAfterBootstrap((e) => {});
|
||||||
onAfterBootstrap((e) => {});
|
onAfterBootstrap((e) => {});
|
||||||
/** Reset instance status to idle on start */
|
/** Reset instance status to idle on start */
|
||||||
onAfterBootstrap((e) => {
|
onAfterBootstrap((e) => {
|
||||||
return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e);
|
return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e);
|
||||||
});
|
});
|
||||||
/** Migrate existing cnames to domains table */
|
|
||||||
onAfterBootstrap((e) => {
|
|
||||||
return require(`${__hooks}/mothership`).HandleMigrateCnamesToDomains(e);
|
|
||||||
});
|
|
||||||
/** Validate instance version */
|
|
||||||
onModelBeforeUpdate((e) => {
|
|
||||||
return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e);
|
|
||||||
}, "instances");
|
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region src/lib/handlers/lemon/hooks.ts
|
//#region src/lib/handlers/lemon/hooks.ts
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export const HandleInstanceResolve = (c: echo.Context) => {
|
|||||||
|
|
||||||
const log = mkLog(`GET:instance/resolve`)
|
const log = mkLog(`GET:instance/resolve`)
|
||||||
|
|
||||||
log(`***TOP OF GET`)
|
log(`TOP OF GET`)
|
||||||
const host = c.queryParam('host')
|
const host = c.queryParam('host')
|
||||||
|
|
||||||
if (!host) {
|
if (!host) {
|
||||||
|
|||||||
@ -1,6 +1,62 @@
|
|||||||
import { mkLog, StringKvLookup } from '$util/Logger'
|
import { mkLog, StringKvLookup } from '$util/Logger'
|
||||||
import { removeEmptyKeys } from '$util/removeEmptyKeys'
|
import { removeEmptyKeys } from '$util/removeEmptyKeys'
|
||||||
|
|
||||||
|
// Helper function to make Cloudflare API calls
|
||||||
|
const callCloudflareAPI = (endpoint: string, method: string, body?: any, log?: any) => {
|
||||||
|
const apiToken = $os.getenv('MOTHERSHIP_CLOUDFLARE_API_TOKEN')
|
||||||
|
const zoneId = $os.getenv('MOTHERSHIP_CLOUDFLARE_ZONE_ID')
|
||||||
|
|
||||||
|
if (!apiToken || !zoneId) {
|
||||||
|
if (log) log('Cloudflare API credentials not configured - skipping Cloudflare operations')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/${endpoint}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config: any = {
|
||||||
|
url: url,
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
config.body = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log) log(`Making Cloudflare API call: ${method} ${url}`, config)
|
||||||
|
|
||||||
|
const response = $http.send(config)
|
||||||
|
|
||||||
|
if (log) log(`Cloudflare API response:`, response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
if (log) log(`Cloudflare API error:`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create custom hostname in Cloudflare
|
||||||
|
const createCloudflareCustomHostname = (hostname: string, log: any) => {
|
||||||
|
return callCloudflareAPI(
|
||||||
|
'custom_hostnames',
|
||||||
|
'POST',
|
||||||
|
{
|
||||||
|
hostname: hostname,
|
||||||
|
ssl: {
|
||||||
|
method: 'http',
|
||||||
|
type: 'dv',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
log
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const HandleInstanceUpdate = (c: echo.Context) => {
|
export const HandleInstanceUpdate = (c: echo.Context) => {
|
||||||
const dao = $app.dao()
|
const dao = $app.dao()
|
||||||
const log = mkLog(`PUT:instance`)
|
const log = mkLog(`PUT:instance`)
|
||||||
@ -67,6 +123,21 @@ export const HandleInstanceUpdate = (c: echo.Context) => {
|
|||||||
throw new BadRequestError(`Not authorized`)
|
throw new BadRequestError(`Not authorized`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if CNAME changed and handle Cloudflare
|
||||||
|
const oldCname = record.getString('cname').trim()
|
||||||
|
const newCname = cname ? cname.trim() : ''
|
||||||
|
const cnameChanged = oldCname !== newCname
|
||||||
|
|
||||||
|
if (cnameChanged && newCname) {
|
||||||
|
log(`CNAME changed from "${oldCname}" to "${newCname}" - adding to Cloudflare`)
|
||||||
|
|
||||||
|
// Blindly add to Cloudflare
|
||||||
|
const createResponse = createCloudflareCustomHostname(newCname, log)
|
||||||
|
if (createResponse) {
|
||||||
|
log(`Cloudflare API call completed for "${newCname}" - frontend will poll for health`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sanitized = removeEmptyKeys({
|
const sanitized = removeEmptyKeys({
|
||||||
subdomain,
|
subdomain,
|
||||||
version,
|
version,
|
||||||
|
|||||||
@ -31,14 +31,20 @@ routerAdd(
|
|||||||
$apis.requireAdminAuth()
|
$apis.requireAdminAuth()
|
||||||
)
|
)
|
||||||
/** Validate instance version */
|
/** Validate instance version */
|
||||||
onModelBeforeCreate((e) => {
|
onModelBeforeUpdate((e) => {
|
||||||
return require(`${__hooks}/mothership`).HandleInstanceVersionValidation(e)
|
return require(`${__hooks}/mothership`).BeforeUpdate_version(e)
|
||||||
}, 'instances')
|
}, 'instances')
|
||||||
|
|
||||||
onModelAfterCreate((e) => {
|
/** Validate cname */
|
||||||
// return require(`${__hooks}/mothership`).HandleNotifyDiscordAfterCreate(e)
|
onModelBeforeUpdate((e) => {
|
||||||
|
return require(`${__hooks}/mothership`).BeforeUpdate_cname(e)
|
||||||
}, 'instances')
|
}, 'instances')
|
||||||
|
|
||||||
|
/** Notify discord on instance create */
|
||||||
|
// onModelAfterCreate((e) => {
|
||||||
|
// return require(`${__hooks}/mothership`).AfterCreate_notify_discord(e)
|
||||||
|
// }, 'instances')
|
||||||
|
|
||||||
onAfterBootstrap((e) => {
|
onAfterBootstrap((e) => {
|
||||||
// return require(`${__hooks}/mothership`).HandleMigrateInstanceVersions(e)
|
// return require(`${__hooks}/mothership`).HandleMigrateInstanceVersions(e)
|
||||||
})
|
})
|
||||||
@ -51,13 +57,3 @@ onAfterBootstrap((e) => {
|
|||||||
onAfterBootstrap((e) => {
|
onAfterBootstrap((e) => {
|
||||||
return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e)
|
return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Migrate existing cnames to domains table */
|
|
||||||
onAfterBootstrap((e) => {
|
|
||||||
return require(`${__hooks}/mothership`).HandleMigrateCnamesToDomains(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Validate instance version */
|
|
||||||
onModelBeforeUpdate((e) => {
|
|
||||||
return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e)
|
|
||||||
}, 'instances')
|
|
||||||
|
|||||||
@ -6,6 +6,6 @@ export * from './bootstrap/HandleInstancesResetIdle'
|
|||||||
export * from './bootstrap/HandleMigrateCnamesToDomains'
|
export * from './bootstrap/HandleMigrateCnamesToDomains'
|
||||||
export * from './bootstrap/HandleMigrateInstanceVersions'
|
export * from './bootstrap/HandleMigrateInstanceVersions'
|
||||||
export * from './bootstrap/HandleMigrateRegions'
|
export * from './bootstrap/HandleMigrateRegions'
|
||||||
export * from './model/HandleInstanceBeforeUpdate'
|
export * from './model/AfterCreate_notify_discord'
|
||||||
export * from './model/HandleInstanceVersionValidation'
|
export * from './model/BeforeUpdate_cname'
|
||||||
export * from './model/HandleNotifyDiscordAfterCreate'
|
export * from './model/BeforeUpdate_version'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { mkLog } from '$util/Logger'
|
import { mkLog } from '$util/Logger'
|
||||||
import { mkAudit } from '$util/mkAudit'
|
import { mkAudit } from '$util/mkAudit'
|
||||||
|
|
||||||
export const HandleNotifyDiscordAfterCreate = (e: core.ModelEvent) => {
|
export const AfterCreate_notify_discord = (e: core.ModelEvent) => {
|
||||||
const dao = e.dao || $app.dao()
|
const dao = e.dao || $app.dao()
|
||||||
|
|
||||||
const log = mkLog(`instances:create:discord:notify`)
|
const log = mkLog(`instances:create:discord:notify`)
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { mkLog } from '$util/Logger'
|
||||||
|
|
||||||
|
export const BeforeUpdate_cname = (e: core.ModelEvent) => {
|
||||||
|
const dao = e.dao || $app.dao()
|
||||||
|
const log = mkLog(`BeforeUpdate_cname`)
|
||||||
|
const id = e.model.getId()
|
||||||
|
const newCname = e.model.get('cname').trim()
|
||||||
|
|
||||||
|
// Only check if cname is already in use locally
|
||||||
|
if (newCname.length > 0) {
|
||||||
|
const result = new DynamicModel({
|
||||||
|
id: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const inUse = (() => {
|
||||||
|
try {
|
||||||
|
dao.db().newQuery(`select id from instances where cname='${newCname}' and id <> '${id}'`).one(result)
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (inUse) {
|
||||||
|
const msg = `[ERROR] [${id}] Custom domain ${newCname} already in use.`
|
||||||
|
log(`${msg}`)
|
||||||
|
throw new BadRequestError(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`CNAME validation passed for: "${newCname}"`)
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { mkLog } from '$util/Logger'
|
||||||
|
import { versions } from '$util/versions'
|
||||||
|
|
||||||
|
export const BeforeUpdate_version = (e: core.ModelEvent) => {
|
||||||
|
const dao = e.dao || $app.dao()
|
||||||
|
|
||||||
|
const log = mkLog(`BeforeUpdate_version`)
|
||||||
|
|
||||||
|
const version = e.model.get('version')
|
||||||
|
log(`Validating version ${version}`)
|
||||||
|
if (!versions.includes(version)) {
|
||||||
|
const msg = `Invalid version ${version}. Version must be one of: ${versions.join(', ')}`
|
||||||
|
log(`[ERROR] ${msg}`)
|
||||||
|
throw new BadRequestError(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import { mkLog } from '$util/Logger'
|
|
||||||
import { versions } from '$util/versions'
|
|
||||||
|
|
||||||
export const HandleInstanceBeforeUpdate = (e: core.ModelEvent) => {
|
|
||||||
const dao = e.dao || $app.dao()
|
|
||||||
|
|
||||||
const log = mkLog(`instances-validate-before-update`)
|
|
||||||
|
|
||||||
const id = e.model.getId()
|
|
||||||
|
|
||||||
const version = e.model.get('version')
|
|
||||||
if (!versions.includes(version)) {
|
|
||||||
const msg = `[ERROR] Invalid version '${version}' for [${id}]. Version must be one of: ${versions.join(', ')}`
|
|
||||||
log(`${msg}`)
|
|
||||||
throw new BadRequestError(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cname = e.model.get('cname')
|
|
||||||
if (cname.length > 0) {
|
|
||||||
const result = new DynamicModel({
|
|
||||||
id: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const inUse = (() => {
|
|
||||||
try {
|
|
||||||
dao.db().newQuery(`select id from instances where cname='${cname}' and id <> '${id}'`).one(result)
|
|
||||||
} catch (e) {
|
|
||||||
// log(` cname OK ${cname}`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})()
|
|
||||||
|
|
||||||
if (inUse) {
|
|
||||||
const msg = `[ERROR] [${id}] Custom domain ${cname} already in use.`
|
|
||||||
log(`${msg}`)
|
|
||||||
throw new BadRequestError(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { versions } from '$util/versions'
|
|
||||||
|
|
||||||
export const HandleInstanceVersionValidation = (e: core.ModelEvent) => {
|
|
||||||
const dao = e.dao || $app.dao()
|
|
||||||
|
|
||||||
const version = e.model.get('version')
|
|
||||||
if (!versions.includes(version)) {
|
|
||||||
throw new BadRequestError(`Invalid version ${version}. Version must be one of: ${versions.join(', ')}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -254,9 +254,6 @@ export const instanceService = mkSingleton(async (config: InstanceServiceConfig)
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (instance) {
|
if (instance) {
|
||||||
if (!instance.cname_active) {
|
|
||||||
throw new Error(`CNAME blocked.`)
|
|
||||||
}
|
|
||||||
dbg(`${host} is a cname`)
|
dbg(`${host} is a cname`)
|
||||||
cache.setItem(instance)
|
cache.setItem(instance)
|
||||||
return instance
|
return instance
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user