diff --git a/gitbook/SUMMARY.md b/gitbook/SUMMARY.md index 53f8e129..ad48ba68 100644 --- a/gitbook/SUMMARY.md +++ b/gitbook/SUMMARY.md @@ -22,6 +22,7 @@ - [Overview](development/overview.md) - [Running Just the Frontend](development/frontend.md) - [Running Everything](development/full-stack/index.md) +- [Creating RPC Calls](development/rpc.md) - [Production Deployment](development/production.md) ## Release History diff --git a/gitbook/development/rpc.md b/gitbook/development/rpc.md new file mode 100644 index 00000000..c403cfc7 --- /dev/null +++ b/gitbook/development/rpc.md @@ -0,0 +1,19 @@ +# Creating RPC Calls + +For security, PocketHost does not allow modification of records by the frontend PocketBase client. Instead, the frontend must send an rpc request which the backend will securely process. This allows for many security vulnerabilities to be addressed which PocketBase admin security rules cannot. In particular, PocketBase admin security rules fall short in these scenarios: + +- When the incoming data cannot be validated declaratively +- When multiple records and/or tables must be updated as a transaction +- When side-effects (ie, other mutations) are required under specific conditions + +Therefore, PocketHost uses an RPC pattern instead. + +## Creating a new RPC Call + +1. Create a new RPC call in `./packages/common/schema/Rpc` +2. Add frontend support in `./packages/pockethost.io/src/pocketbase/PocketbaseClient.ts` using the `mkRpc` command +3. Add backend support in (for example) `./packages/daemon/src/services/InstanceService/InstanceService.ts` using `registerCommand` + +## Getting the result from an RPC call + +RPC results are currently not supported. RPC commands are run asynchronously. diff --git a/packages/common/src/pocketbase-client-helpers/RpcHelper.ts b/packages/common/src/pocketbase-client-helpers/RpcHelper.ts index cef8d18f..9608638e 100644 --- a/packages/common/src/pocketbase-client-helpers/RpcHelper.ts +++ b/packages/common/src/pocketbase-client-helpers/RpcHelper.ts @@ -39,6 +39,7 @@ export const createRpcHelper = (config: RpcHelperConfig) => { const validator = new Ajv().compile(schema) return safeCatch( cmd, + logger(), async ( payload: TPayload, cb?: (data: RecordSubscription) => void diff --git a/packages/common/src/schema/Rpc/SaveSecrets.ts b/packages/common/src/schema/Rpc/SaveSecrets.ts index 609e1be9..c72ecef9 100644 --- a/packages/common/src/schema/Rpc/SaveSecrets.ts +++ b/packages/common/src/schema/Rpc/SaveSecrets.ts @@ -9,7 +9,8 @@ export type SaveSecretsPayload = { } export type SaveSecretsResult = { - status: 'saved' + status: 'ok' | 'error' + message?: string } export const SECRET_KEY_REGEX = /^[A-Z][A-Z0-9_]*$/ diff --git a/packages/common/src/schema/Rpc/SaveVersion.ts b/packages/common/src/schema/Rpc/SaveVersion.ts new file mode 100644 index 00000000..5d560cf5 --- /dev/null +++ b/packages/common/src/schema/Rpc/SaveVersion.ts @@ -0,0 +1,24 @@ +import { JSONSchemaType } from 'ajv' +import { InstanceId, Semver } from '../types' + +export type SaveVersionPayload = { + instanceId: InstanceId + version: Semver +} + +export type SaveVersionResult = { + status: 'ok' | 'error' + message?: string +} + +export const SaveVersionPayloadSchema: JSONSchemaType = { + type: 'object', + properties: { + instanceId: { type: 'string' }, + version: { + type: 'string', + }, + }, + required: ['instanceId', 'version'], + additionalProperties: false, +} diff --git a/packages/common/src/schema/Rpc/index.ts b/packages/common/src/schema/Rpc/index.ts index 470872f1..8626e7a6 100644 --- a/packages/common/src/schema/Rpc/index.ts +++ b/packages/common/src/schema/Rpc/index.ts @@ -6,12 +6,14 @@ export const RPC_COLLECTION = 'rpc' export enum RpcCommands { CreateInstance = 'create-instance', SaveSecrets = 'save-secrets', + SaveVersion = 'save-version', // gen:enum } export const RPC_COMMANDS = [ RpcCommands.CreateInstance, RpcCommands.SaveSecrets, + RpcCommands.SaveVersion, ] export enum RpcStatus { @@ -46,4 +48,5 @@ export const ajv = new Ajv() export * from './CreateInstance' export * from './SaveSecrets' +export * from './SaveVersion' // gen:export diff --git a/packages/common/src/schema/types.ts b/packages/common/src/schema/types.ts index 5220b107..03c5081b 100644 --- a/packages/common/src/schema/types.ts +++ b/packages/common/src/schema/types.ts @@ -1,6 +1,7 @@ export type RecordId = string export type UserId = RecordId export type InstanceId = RecordId +export type Semver = string export type InternalInstanceId = RecordId export type Subdomain = string export type Port = number diff --git a/packages/daemon/src/services/InstanceService/InstanceService.ts b/packages/daemon/src/services/InstanceService/InstanceService.ts index eefd1db5..c7280fdc 100644 --- a/packages/daemon/src/services/InstanceService/InstanceService.ts +++ b/packages/daemon/src/services/InstanceService/InstanceService.ts @@ -22,6 +22,9 @@ import { SaveSecretsPayload, SaveSecretsPayloadSchema, SaveSecretsResult, + SaveVersionPayload, + SaveVersionPayloadSchema, + SaveVersionResult, SingletonBaseConfig, } from '@pockethost/common' import { forEachRight, map } from '@s-libs/micro-dash' @@ -75,6 +78,22 @@ export const instanceService = mkSingleton( } ) + const SEMVER_RE = + /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?$/ + registerCommand( + RpcCommands.SaveVersion, + SaveVersionPayloadSchema, + async (rpc) => { + const { payload } = rpc + const { instanceId, version } = payload + if (version.match(SEMVER_RE) === null) { + return { status: `error`, message: `Semver must be a regex` } + } + await client.updateInstance(instanceId, { version }) + return { status: 'ok' } + } + ) + registerCommand( RpcCommands.SaveSecrets, SaveSecretsPayloadSchema, @@ -82,7 +101,7 @@ export const instanceService = mkSingleton( const { payload } = job const { instanceId, secrets } = payload await client.updateInstance(instanceId, { secrets }) - return { status: 'saved' } + return { status: 'ok' } } ) diff --git a/packages/pockethost.io/src/pocketbase/PocketbaseClient.ts b/packages/pockethost.io/src/pocketbase/PocketbaseClient.ts index 812f2f34..482ca295 100644 --- a/packages/pockethost.io/src/pocketbase/PocketbaseClient.ts +++ b/packages/pockethost.io/src/pocketbase/PocketbaseClient.ts @@ -9,12 +9,15 @@ import { RpcCommands, safeCatch, SaveSecretsPayloadSchema, + SaveVersionPayloadSchema, type CreateInstancePayload, type CreateInstanceResult, type InstanceFields, type InstanceId, type SaveSecretsPayload, type SaveSecretsResult, + type SaveVersionPayload, + type SaveVersionResult, type UserFields, type WorkerLogFields } from '@pockethost/common' @@ -123,6 +126,11 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => { SaveSecretsPayloadSchema ) + const saveVersion = mkRpc( + RpcCommands.SaveVersion, + SaveVersionPayloadSchema + ) + const getInstanceById = safeCatch( `getInstanceById`, _logger, @@ -291,6 +299,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => { user, watchInstanceById, getAllInstancesById, - resendVerificationEmail + resendVerificationEmail, + saveVersion } } diff --git a/packages/pockethost.io/src/routes/app/instances/[instanceId]/Overview.svelte b/packages/pockethost.io/src/routes/app/instances/[instanceId]/Overview.svelte index 8f6acaec..4a70c09d 100644 --- a/packages/pockethost.io/src/routes/app/instances/[instanceId]/Overview.svelte +++ b/packages/pockethost.io/src/routes/app/instances/[instanceId]/Overview.svelte @@ -2,6 +2,7 @@ import ProvisioningStatus from '$components/ProvisioningStatus.svelte' import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env' import type { InstanceFields } from '@pockethost/common' + import RunningStatus from './RunningStatus.svelte' export let instance: InstanceFields @@ -13,9 +14,7 @@

Overview

Usage: {Math.ceil(instance.secondsThisMonth / 60)} mins -
- Running {version} -
+
Admin URL: {`${url}/_`}
diff --git a/packages/pockethost.io/src/routes/app/instances/[instanceId]/RunningStatus.svelte b/packages/pockethost.io/src/routes/app/instances/[instanceId]/RunningStatus.svelte new file mode 100644 index 00000000..34176ee0 --- /dev/null +++ b/packages/pockethost.io/src/routes/app/instances/[instanceId]/RunningStatus.svelte @@ -0,0 +1,69 @@ + + +
+ Running {#if !editMode} + {_version} + + {msg} + {/if} + {#if editMode} + + + + {/if} +