mirror of
https://github.com/pockethost/pockethost.git
synced 2025-06-27 16:32:32 +00:00
enh: admin sync
This commit is contained in:
parent
a4aeede347
commit
4751d0c16e
@ -4,6 +4,7 @@
|
||||
import { INSTANCE_ADMIN_URL } from '$src/env'
|
||||
import { slide } from 'svelte/transition'
|
||||
import Code from './Code.svelte'
|
||||
import AdminSync from './Danger/AdminSync.svelte'
|
||||
import DangerZoneTitle from './Danger/DangerZoneTitle.svelte'
|
||||
import Maintenance from './Danger/Maintenance.svelte'
|
||||
import RenameInstance from './Danger/RenameInstance.svelte'
|
||||
@ -84,4 +85,6 @@
|
||||
<Maintenance />
|
||||
|
||||
<VersionChange />
|
||||
|
||||
<AdminSync />
|
||||
</div>
|
||||
|
@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import Card from '$components/cards/Card.svelte'
|
||||
import CardHeader from '$components/cards/CardHeader.svelte'
|
||||
import { DOCS_URL } from '$src/env'
|
||||
import { client } from '$src/pocketbase-client'
|
||||
import { instance } from '../store'
|
||||
|
||||
const { updateInstance } = client()
|
||||
|
||||
$: ({ id, syncAdmin } = $instance)
|
||||
|
||||
const handleChange = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const isChecked = target.checked
|
||||
|
||||
// Update the database with the new value
|
||||
updateInstance({ id, fields: { syncAdmin: isChecked } }).then(() => 'saved')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardHeader documentation={DOCS_URL(`/usage/admin-sync`)}>
|
||||
Admin Sync
|
||||
</CardHeader>
|
||||
|
||||
<p class="mb-8">
|
||||
Your instance will have an admin login that matches your pockethost.io
|
||||
login.
|
||||
</p>
|
||||
|
||||
<label class="label cursor-pointer justify-center gap-4">
|
||||
<span class="label-text">Admin Sync</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-warning"
|
||||
checked={!!syncAdmin}
|
||||
on:change={handleChange}
|
||||
/>
|
||||
</label>
|
||||
</Card>
|
19
frontends/lander/content/docs/usage/admin-sync.md
Normal file
19
frontends/lander/content/docs/usage/admin-sync.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Admin Sync
|
||||
category: usage
|
||||
description: Admin Sync keeps your instance secure by syncing your pockethost.io account with the admin account of your instance.
|
||||
---
|
||||
|
||||
Admin Sync will make sure your instance always has an admin account that matches the login credentials of your pockethost.io account.
|
||||
|
||||
## Admin Sync `Enabled` (default)
|
||||
|
||||
When Admin Sync is enabled, your pockethost.io account credentials will be copied as an admin login to your instance before the instance is launched.
|
||||
|
||||
If you change your pockethost.io credentials while an instance is running, it will not be updated until the next time it is launched. To force your instance to shut down, place it in Maintenance Mode and then wait for the status to show as `idle`.
|
||||
|
||||
Admin Sync is enabeld by default. When an instance is first created, it will have an admin account matching your pockethost.io login. This is a security measure to prevent someone from creating the initial admin account before you've had a chance.
|
||||
|
||||
## Admin Sync `Disabled`
|
||||
|
||||
Your pockethost.io credentails will not be copied to your instance on future invocations.
|
@ -205,10 +205,12 @@ export const INSTANCE_APP_MIGRATIONS_DIR = () =>
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
export const MOTHERSHIP_DATA_ROOT = () =>
|
||||
join(settings().DATA_ROOT, settings().MOTHERSHIP_NAME)
|
||||
export const mkAppUrl = (path = '') => `${settings().APP_URL}${path}`
|
||||
export const mkBlogUrl = (path = '') => `${settings().BLOG_URL}${path}`
|
||||
export const MOTHERSHIP_DATA_ROOT = () => INSTANCE_DATA_ROOT(MOTHERSHIP_NAME())
|
||||
export const INSTANCE_DATA_ROOT = (id: InstanceId) => join(DATA_ROOT(), id)
|
||||
export const INSTANCE_DATA_DB = (id: InstanceId) =>
|
||||
join(DATA_ROOT(), id, `pb_data`, `data.db`)
|
||||
export const mkAppUrl = (path = '') => `${APP_URL()}${path}`
|
||||
export const mkBlogUrl = (path = '') => `${BLOG_URL()}${path}`
|
||||
export const mkDocUrl = (path = '') => mkBlogUrl(join('/docs', path))
|
||||
export const mkEdgeSubdomain = (subdomain: string) =>
|
||||
`${settings().HTTP_PROTOCOL}//${subdomain}.${settings().EDGE_APEX_DOMAIN}`
|
||||
|
@ -0,0 +1,27 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate((db) => {
|
||||
const dao = new Dao(db)
|
||||
const collection = dao.findCollectionByNameOrId("etae8tuiaxl6xfv")
|
||||
|
||||
// add
|
||||
collection.schema.addField(new SchemaField({
|
||||
"system": false,
|
||||
"id": "hkt4q8yk",
|
||||
"name": "syncAdmin",
|
||||
"type": "bool",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {}
|
||||
}))
|
||||
|
||||
return dao.saveCollection(collection)
|
||||
}, (db) => {
|
||||
const dao = new Dao(db)
|
||||
const collection = dao.findCollectionByNameOrId("etae8tuiaxl6xfv")
|
||||
|
||||
// remove
|
||||
collection.schema.removeField("hkt4q8yk")
|
||||
|
||||
return dao.saveCollection(collection)
|
||||
})
|
@ -48,6 +48,7 @@ routerAdd(
|
||||
record.set('subdomain', subdomain)
|
||||
record.set('status', 'idle')
|
||||
record.set('version', versions[0])
|
||||
record.set('syncAdmin', true)
|
||||
|
||||
const form = new RecordUpsertForm($app, record)
|
||||
form.submit()
|
||||
|
@ -22,6 +22,7 @@ routerAdd(
|
||||
name: null,
|
||||
version: null,
|
||||
secrets: null,
|
||||
syncAdmin: null,
|
||||
},
|
||||
})
|
||||
|
||||
@ -33,7 +34,7 @@ routerAdd(
|
||||
|
||||
const id = c.pathParam('id')
|
||||
const {
|
||||
fields: { maintenance, name, version, secrets },
|
||||
fields: { maintenance, name, version, secrets, syncAdmin },
|
||||
} = data
|
||||
|
||||
console.log(
|
||||
@ -44,6 +45,7 @@ routerAdd(
|
||||
name,
|
||||
version,
|
||||
secrets,
|
||||
syncAdmin,
|
||||
}),
|
||||
)
|
||||
|
||||
@ -75,6 +77,7 @@ routerAdd(
|
||||
version,
|
||||
maintenance,
|
||||
secrets,
|
||||
syncAdmin,
|
||||
})
|
||||
|
||||
const form = new RecordUpsertForm($app, record)
|
||||
|
29
src/mothership-app/pb_hooks/src/user-token.pb.js
Normal file
29
src/mothership-app/pb_hooks/src/user-token.pb.js
Normal file
@ -0,0 +1,29 @@
|
||||
/// <reference path="../types/types.d.ts" />
|
||||
|
||||
/*
|
||||
{
|
||||
"id": "user-id"
|
||||
}
|
||||
*/
|
||||
routerAdd(
|
||||
'GET',
|
||||
'/api/userToken/:id',
|
||||
(c) => {
|
||||
const id = c.pathParam('id')
|
||||
|
||||
console.log(`***vars`, JSON.stringify({ id }))
|
||||
|
||||
if (!id) {
|
||||
throw new BadRequestError(`User ID is required.`)
|
||||
}
|
||||
|
||||
const rec = $app.dao().findRecordById('users', id)
|
||||
const tokenKey = rec.getString('tokenKey')
|
||||
const passwordHash = rec.getString('passwordHash')
|
||||
const email = rec.getString(`email`)
|
||||
console.log(`***tokenKey`, tokenKey)
|
||||
|
||||
return c.json(200, { email, passwordHash, tokenKey })
|
||||
},
|
||||
$apis.requireAdminAuth(),
|
||||
)
|
@ -2,6 +2,7 @@ import {
|
||||
DAEMON_PB_IDLE_TTL,
|
||||
INSTANCE_APP_HOOK_DIR,
|
||||
INSTANCE_APP_MIGRATIONS_DIR,
|
||||
INSTANCE_DATA_DB,
|
||||
mkAppUrl,
|
||||
mkDocUrl,
|
||||
mkEdgeUrl,
|
||||
@ -13,6 +14,7 @@ import {
|
||||
PocketbaseService,
|
||||
PortService,
|
||||
proxyService,
|
||||
SqliteService,
|
||||
} from '$services'
|
||||
import {
|
||||
assertTruthy,
|
||||
@ -244,10 +246,31 @@ export const instanceService = mkSingleton(
|
||||
})
|
||||
healthyGuard()
|
||||
|
||||
/**
|
||||
* Sync admin account
|
||||
*/
|
||||
if (instance.syncAdmin) {
|
||||
const id = instance.uid
|
||||
dbg(`Fetching token info for uid ${id}`)
|
||||
const { email, tokenKey, passwordHash } =
|
||||
await client.getUserTokenInfo({ id })
|
||||
dbg(`Token info is`, { email, tokenKey, passwordHash })
|
||||
const sqliteService = await SqliteService()
|
||||
const db = await sqliteService.getDatabase(
|
||||
INSTANCE_DATA_DB(instance.id),
|
||||
)
|
||||
await db(`_admins`)
|
||||
.insert({ id, email, tokenKey, passwordHash })
|
||||
.onConflict('id')
|
||||
.merge({ email, tokenKey, passwordHash })
|
||||
.catch((e) => {
|
||||
userInstanceLogger.error(`Failed to sync admin account: ${e}`)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Spawn the child process
|
||||
*/
|
||||
|
||||
const childProcess = await (async () => {
|
||||
try {
|
||||
const cp = await pbService.spawn({
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { LoggerService } from '$shared'
|
||||
import {
|
||||
GetUserTokenPayload,
|
||||
GetUserTokenPayloadSchema,
|
||||
GetUserTokenResult,
|
||||
LoggerService,
|
||||
RestCommands,
|
||||
RestMethods,
|
||||
createRestHelper,
|
||||
} from '$shared'
|
||||
import { default as PocketBase } from 'pocketbase'
|
||||
import { MixinContext } from '.'
|
||||
import { createInstanceMixin } from './InstanceMIxin'
|
||||
@ -29,11 +37,21 @@ export const createAdminPbClient = (url: string) => {
|
||||
const context: MixinContext = { client, logger: _clientLogger }
|
||||
const instanceApi = createInstanceMixin(context)
|
||||
|
||||
const restHelper = createRestHelper({ client })
|
||||
const { mkRest } = restHelper
|
||||
|
||||
const getUserTokenInfo = mkRest<GetUserTokenPayload, GetUserTokenResult>(
|
||||
RestCommands.UserToken,
|
||||
RestMethods.Get,
|
||||
GetUserTokenPayloadSchema,
|
||||
)
|
||||
|
||||
const api = {
|
||||
client,
|
||||
url,
|
||||
createFirstAdmin,
|
||||
adminAuthViaEmail,
|
||||
getUserTokenInfo,
|
||||
...instanceApi,
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ export type InstanceFields = BaseFields & {
|
||||
version: VersionId
|
||||
secrets: InstanceSecretCollection | null
|
||||
maintenance: boolean
|
||||
syncAdmin: boolean
|
||||
}
|
||||
|
||||
export type WithUser = {
|
||||
|
22
src/shared/schema/Rest/GetUserTokenInfo.ts
Normal file
22
src/shared/schema/Rest/GetUserTokenInfo.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { JSONSchemaType } from 'ajv'
|
||||
import { UserId } from '../types'
|
||||
|
||||
export type GetUserTokenPayload = {
|
||||
id: UserId
|
||||
}
|
||||
|
||||
export type GetUserTokenResult = {
|
||||
id: string
|
||||
email: string
|
||||
tokenKey: string
|
||||
passwordHash: string
|
||||
}
|
||||
|
||||
export const GetUserTokenPayloadSchema: JSONSchemaType<GetUserTokenPayload> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
},
|
||||
required: ['id'],
|
||||
additionalProperties: false,
|
||||
}
|
@ -9,6 +9,7 @@ export enum RestMethods {
|
||||
|
||||
export enum RestCommands {
|
||||
Instance = 'instance',
|
||||
UserToken = 'userToken',
|
||||
}
|
||||
|
||||
export type RestPayloadBase = JsonObject
|
||||
@ -16,4 +17,5 @@ export type RestPayloadBase = JsonObject
|
||||
export const ajv = new Ajv()
|
||||
|
||||
export * from './CreateInstance'
|
||||
export * from './GetUserTokenInfo'
|
||||
export * from './UpdateInstance'
|
||||
|
Loading…
x
Reference in New Issue
Block a user