mirror of
https://github.com/pockethost/pockethost.git
synced 2025-05-19 13:26:42 +00:00
feat: update pocketbase semver
This commit is contained in:
parent
465b95b5bc
commit
7b1d2d473c
@ -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
|
||||
|
19
gitbook/development/rpc.md
Normal file
19
gitbook/development/rpc.md
Normal file
@ -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.
|
@ -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<ConcreteRpcRecord>) => void
|
||||
|
@ -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_]*$/
|
||||
|
24
packages/common/src/schema/Rpc/SaveVersion.ts
Normal file
24
packages/common/src/schema/Rpc/SaveVersion.ts
Normal file
@ -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<SaveVersionPayload> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
instanceId: { type: 'string' },
|
||||
version: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['instanceId', 'version'],
|
||||
additionalProperties: false,
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<SaveVersionPayload, SaveVersionResult>(
|
||||
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<SaveSecretsPayload, SaveSecretsResult>(
|
||||
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' }
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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<SaveVersionPayload, SaveVersionResult>(
|
||||
RpcCommands.SaveVersion,
|
||||
SaveVersionPayloadSchema
|
||||
)
|
||||
|
||||
const getInstanceById = safeCatch(
|
||||
`getInstanceById`,
|
||||
_logger,
|
||||
@ -291,6 +299,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
|
||||
user,
|
||||
watchInstanceById,
|
||||
getAllInstancesById,
|
||||
resendVerificationEmail
|
||||
resendVerificationEmail,
|
||||
saveVersion
|
||||
}
|
||||
}
|
||||
|
@ -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 @@
|
||||
<h2>Overview</h2>
|
||||
<ProvisioningStatus {status} />
|
||||
Usage: {Math.ceil(instance.secondsThisMonth / 60)} mins
|
||||
<div>
|
||||
Running {version}
|
||||
</div>
|
||||
<RunningStatus {instance} />
|
||||
<div>
|
||||
Admin URL: <a href={`${url}/_`} target="_blank">{`${url}/_`}</a>
|
||||
</div>
|
||||
|
@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
|
||||
import { client } from '$src/pocketbase'
|
||||
import type { InstanceFields } from '@pockethost/common'
|
||||
|
||||
export let instance: InstanceFields
|
||||
|
||||
const { subdomain, status, version } = instance
|
||||
const url = `${PUBLIC_APP_PROTOCOL}://${subdomain}.${PUBLIC_APP_DOMAIN}`
|
||||
|
||||
let msg = ''
|
||||
let _oldVersion = version
|
||||
let _version = version
|
||||
let editMode = false
|
||||
const startEdit = () => {
|
||||
_oldVersion = _version
|
||||
editMode = true
|
||||
}
|
||||
const cancelEdit = () => {
|
||||
_version = _oldVersion
|
||||
editMode = false
|
||||
}
|
||||
|
||||
const saveEdit = () => {
|
||||
client()
|
||||
.saveVersion({ instanceId: instance.id, version: _version })
|
||||
.then(() => {
|
||||
editMode = false
|
||||
msg = 'saved'
|
||||
})
|
||||
.catch((e) => {
|
||||
msg = e.message
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
Running {#if !editMode}
|
||||
{_version}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
style="--bs-btn-padding-y: .05rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
|
||||
on:click={startEdit}
|
||||
>
|
||||
edit
|
||||
</button>
|
||||
{msg}
|
||||
{/if}
|
||||
{#if editMode}
|
||||
<input type="text" bind:value={_version} />
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success"
|
||||
style="--bs-btn-padding-y: .05rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
|
||||
on:click={saveEdit}
|
||||
>
|
||||
save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
style="--bs-btn-padding-y: .05rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
|
||||
on:click={cancelEdit}
|
||||
>
|
||||
cancel
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
Loading…
x
Reference in New Issue
Block a user