Rebuild API Server on the PilotVault structure
Mirror PilotVault's API Server layout and add the superadmin console,
plugin system, runtime PocketBase settings, and user/organization
management. The car domain (cars, service records, parts, sharing) is
carried over unchanged apart from the auth switch.
Layout: main.go -> cmd/server/main.go; module carcontrol/api ->
drivervault/apiserver. internal/api is split by concern (auth, users,
orgs, settings, plugins, status, health, respond).
Auth: replace the server-minted HS256 JWT and the sessions collection
with a PocketBase token proxy. /api/auth/login relays PocketBase's
{token, record}, and every protected request re-resolves that token
against PocketBase, so a role change or deletion takes effect at once
instead of waiting out a token. AUTH_SECRET is obsolete and internal/auth
is gone. Per-device session listing/revocation goes with it: PocketBase
tokens are stateless. Changing a password rotates the user's token key,
which invalidates every token already issued.
Roles: add superadmin alongside user/admin, plus an organizations
collection and users.organization. Admins are scoped to their own
organization; superadmins span all of them. Guards prevent changing your
own role, deleting your own account, an admin touching a superadmin, and
deleting an organization that still has members.
Plugins: new internal/plugins package with one contract over two kinds --
builtin (compiled in) and external (any HTTP service, registered at
runtime with no rebuild). State persists to plugins.json; secrets are
masked on read and preserved when saved back at the mask.
PocketBase settings: /api/admin/pb-config applies a new connection at
runtime and persists it to .env. It deliberately does not require a
working service account, so a wrong or unreachable connection can still
be fixed from the panel.
Panel: rebuilt as the superadmin console -- login gate, status, users,
organizations, PocketBase, plugins, and the endpoint reference.
Clients: update the Web App and Phone App for the PocketBase token shape,
the move of user management to /api/users ({users}/{user} envelopes, with
password resets folded into PATCH), and the removal of sessions. Both now
mirror the server's real guards rather than the old last-admin rule, and
parse PocketBase's field-level error shape.
Config: modern POCKETBASE_*/API_ADDR names with legacy PB_*/PORT
fallbacks, so existing .env files keep working. Also fixes /api/status
probing the Web App on 8090 instead of DriverVault's 5173.
Run scripts/setup-pocketbase.mjs to add the organizations collection and
grow users.role; every client must log in once more.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7d55f0a4cd
commit
ae6ed4ac1e
@@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { request } from "../api";
|
||||
|
||||
// Superadmin-only: retarget the PocketBase this server talks to. The change is
|
||||
// applied at runtime AND persisted to the server's .env, so it survives a
|
||||
// restart. A blank password means "keep the stored one".
|
||||
const cfg = ref(null);
|
||||
const form = ref({ url: "", adminEmail: "", adminPassword: "" });
|
||||
const probe = ref(null);
|
||||
const error = ref("");
|
||||
const notice = ref("");
|
||||
const busy = ref(false);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
cfg.value = await request("/api/admin/pb-config");
|
||||
form.value = {
|
||||
url: cfg.value.url,
|
||||
adminEmail: cfg.value.adminEmail,
|
||||
adminPassword: "",
|
||||
};
|
||||
probe.value = cfg.value.probe;
|
||||
} catch (e) {
|
||||
error.value = e.message;
|
||||
}
|
||||
}
|
||||
onMounted(load);
|
||||
|
||||
async function test() {
|
||||
error.value = "";
|
||||
notice.value = "";
|
||||
busy.value = true;
|
||||
try {
|
||||
probe.value = await request("/api/admin/pb-config/test", {
|
||||
method: "POST",
|
||||
body: form.value,
|
||||
});
|
||||
notice.value = probe.value.superuser
|
||||
? "Connection OK — superuser authenticated."
|
||||
: probe.value.reachable
|
||||
? "PocketBase is reachable, but the service account did not authenticate."
|
||||
: "PocketBase is not reachable at that address.";
|
||||
} catch (e) {
|
||||
error.value = e.message;
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
error.value = "";
|
||||
notice.value = "";
|
||||
busy.value = true;
|
||||
try {
|
||||
const out = await request("/api/admin/pb-config", { method: "PUT", body: form.value });
|
||||
cfg.value = out.config;
|
||||
probe.value = out.config.probe;
|
||||
form.value.adminPassword = "";
|
||||
notice.value = out.warning || "Saved. The server is now using this PocketBase.";
|
||||
} catch (e) {
|
||||
error.value = e.message;
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dh-card overflow-hidden">
|
||||
<div class="flex items-center justify-between border-b border-subtle px-5 py-4">
|
||||
<div>
|
||||
<div class="text-base font-bold tracking-[-0.02em] text-strong">PocketBase</div>
|
||||
<p class="mt-0.5 text-xs text-muted">Database connection used by every endpoint</p>
|
||||
</div>
|
||||
<span
|
||||
v-if="probe"
|
||||
class="dh-pill"
|
||||
:class="probe.superuser
|
||||
? 'bg-success-soft text-success'
|
||||
: probe.reachable
|
||||
? 'bg-warning-soft text-warning'
|
||||
: 'bg-danger-soft text-danger'"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-current"></span>
|
||||
{{ probe.superuser ? "connected" : probe.reachable ? "no superuser" : "unreachable" }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 px-5 py-4">
|
||||
<div>
|
||||
<label class="dh-label" for="pb-url">Base URL</label>
|
||||
<input id="pb-url" v-model="form.url" class="dh-input" placeholder="http://10.2.1.10:8027" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="dh-label" for="pb-email">Superuser email</label>
|
||||
<input id="pb-email" v-model="form.adminEmail" class="dh-input" autocomplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="dh-label" for="pb-password">Superuser password</label>
|
||||
<input
|
||||
id="pb-password"
|
||||
v-model="form.adminPassword"
|
||||
class="dh-input"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
:placeholder="cfg?.adminConfigured ? 'unchanged' : 'not set'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="probe?.detail" class="data text-xs text-muted">{{ probe.detail }}</p>
|
||||
<p v-if="notice" class="rounded-control bg-info-soft px-3 py-2 text-xs text-info">{{ notice }}</p>
|
||||
<p v-if="error" class="rounded-control bg-danger-soft px-3 py-2 text-xs text-danger">{{ error }}</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="dh-btn" :disabled="busy" @click="save">Save & apply</button>
|
||||
<button class="dh-btn-ghost" :disabled="busy" @click="test">Test connection</button>
|
||||
<span class="flex-1"></span>
|
||||
<span class="eyebrow">persisted to .env</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user