Initial commit: PilotVault multi-service project
Add API Server (Go/PocketBase), Web App (Go BFF + Vue), Fly App (Flutter/DJI MSDK), Adobe Plugin, and Docker/Docker AIO deployment configs. Design assets and build artifacts are gitignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// Adds a `preferences` JSON field to the `users` auth collection so each user
|
||||
// can persist their PilotVault settings (theme, appearance, profile, etc.).
|
||||
//
|
||||
// Apply by copying this file into your PocketBase deployment's `pb_migrations/`
|
||||
// directory and restarting PocketBase (migrations run automatically on boot).
|
||||
// Written for PocketBase v0.22+/v0.23 (JSVM `migrate((app) => …)` API). If your
|
||||
// PocketBase is older, add the field manually — see pocketbase/README.md.
|
||||
migrate(
|
||||
(app) => {
|
||||
const users = app.findCollectionByNameOrId('users')
|
||||
|
||||
users.fields.add(
|
||||
new Field({
|
||||
name: 'preferences',
|
||||
type: 'json',
|
||||
required: false,
|
||||
presentable: false,
|
||||
// ~5 MB — generous headroom (an optional base64 avatar rides along).
|
||||
maxSize: 5000000,
|
||||
}),
|
||||
)
|
||||
|
||||
app.save(users)
|
||||
},
|
||||
(app) => {
|
||||
const users = app.findCollectionByNameOrId('users')
|
||||
users.fields.removeByName('preferences')
|
||||
app.save(users)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// Adds a `role` select field (admin | user) to the `users` auth collection.
|
||||
// Drives PilotVault's user-rights model: admins can add/remove users; the API
|
||||
// Server reads this field from the caller's token to gate admin endpoints.
|
||||
// Missing/empty role is treated as "user" by the app.
|
||||
//
|
||||
// Apply by copying this file into your PocketBase deployment's `pb_migrations/`
|
||||
// directory and restarting PocketBase. Written for PocketBase v0.22+/v0.23.
|
||||
migrate(
|
||||
(app) => {
|
||||
const users = app.findCollectionByNameOrId('users')
|
||||
|
||||
users.fields.add(
|
||||
new Field({
|
||||
name: 'role',
|
||||
type: 'select',
|
||||
required: false,
|
||||
presentable: false,
|
||||
maxSelect: 1,
|
||||
values: ['admin', 'user'],
|
||||
}),
|
||||
)
|
||||
|
||||
app.save(users)
|
||||
},
|
||||
(app) => {
|
||||
const users = app.findCollectionByNameOrId('users')
|
||||
users.fields.removeByName('role')
|
||||
app.save(users)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// Creates the `organizations` collection. PilotVault scopes admins to a single
|
||||
// organization; a superadmin spans all of them. Users may belong to no org.
|
||||
//
|
||||
// The API Server reaches this collection only through its superuser service
|
||||
// account (like `users` management), so the collection API rules are left locked
|
||||
// (superusers only). Apply by copying into your PocketBase deployment's
|
||||
// `pb_migrations/` directory and restarting. Written for PocketBase v0.22+/v0.23.
|
||||
//
|
||||
// Idempotent: if the collection already exists (e.g. it was provisioned live via
|
||||
// the admin API), this migration is a no-op.
|
||||
migrate(
|
||||
(app) => {
|
||||
try {
|
||||
app.findCollectionByNameOrId('organizations')
|
||||
return // already present
|
||||
} catch (_) {
|
||||
// not found → create it
|
||||
}
|
||||
|
||||
const collection = new Collection({
|
||||
type: 'base',
|
||||
name: 'organizations',
|
||||
fields: [
|
||||
{ name: 'name', type: 'text', required: true, max: 120, presentable: true },
|
||||
{ name: 'created', type: 'autodate', onCreate: true, onUpdate: false },
|
||||
{ name: 'updated', type: 'autodate', onCreate: true, onUpdate: true },
|
||||
],
|
||||
indexes: ['CREATE UNIQUE INDEX `idx_org_name` ON `organizations` (`name`)'],
|
||||
})
|
||||
|
||||
app.save(collection)
|
||||
},
|
||||
(app) => {
|
||||
try {
|
||||
const c = app.findCollectionByNameOrId('organizations')
|
||||
app.delete(c)
|
||||
} catch (_) {
|
||||
// already gone
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// Adds an `organization` relation field to the `users` auth collection, pointing
|
||||
// at the `organizations` collection. Not required (maxSelect 1), so users may be
|
||||
// org-less. cascadeDelete is false: deleting an org does not delete its members.
|
||||
//
|
||||
// Depends on 1720300200_add_organizations.js. Idempotent: no-op if the field is
|
||||
// already present. Written for PocketBase v0.22+/v0.23.
|
||||
migrate(
|
||||
(app) => {
|
||||
const users = app.findCollectionByNameOrId('users')
|
||||
if (users.fields.getByName('organization')) return
|
||||
|
||||
const orgs = app.findCollectionByNameOrId('organizations')
|
||||
users.fields.add(
|
||||
new Field({
|
||||
name: 'organization',
|
||||
type: 'relation',
|
||||
required: false,
|
||||
collectionId: orgs.id,
|
||||
cascadeDelete: false,
|
||||
minSelect: 0,
|
||||
maxSelect: 1,
|
||||
presentable: false,
|
||||
}),
|
||||
)
|
||||
app.save(users)
|
||||
},
|
||||
(app) => {
|
||||
const users = app.findCollectionByNameOrId('users')
|
||||
users.fields.removeByName('organization')
|
||||
app.save(users)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// Extends the `users.role` select field with a top-level `superadmin` value.
|
||||
// Final set: superadmin | admin | user. superadmin spans all organizations;
|
||||
// admin is scoped to one org; user has no management rights. Missing/empty role
|
||||
// is still treated as `user` by the app.
|
||||
//
|
||||
// Idempotent: no-op if `superadmin` is already an allowed value. Written for
|
||||
// PocketBase v0.22+/v0.23.
|
||||
migrate(
|
||||
(app) => {
|
||||
const users = app.findCollectionByNameOrId('users')
|
||||
const role = users.fields.getByName('role')
|
||||
if (!role || (role.values && role.values.indexOf('superadmin') !== -1)) return
|
||||
role.values = ['superadmin'].concat(role.values || [])
|
||||
app.save(users)
|
||||
},
|
||||
(app) => {
|
||||
const users = app.findCollectionByNameOrId('users')
|
||||
const role = users.fields.getByName('role')
|
||||
if (!role) return
|
||||
role.values = (role.values || []).filter((v) => v !== 'superadmin')
|
||||
app.save(users)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// Seeds PilotVault's baseline org + accounts. Idempotent: existing records are
|
||||
// left in place (org/role are reconciled, passwords are not touched once a user
|
||||
// exists). Safe to run alongside a live-provisioned deployment.
|
||||
//
|
||||
// Organization: PilotVault
|
||||
// superadmin@pilotvault.local (superadmin, no org)
|
||||
// dariusz@pilotvault.local (admin, PilotVault) — expected to pre-exist
|
||||
// pilot@pilotvault.local (user, PilotVault)
|
||||
// pilot@dji.local (user, no org) — left untouched
|
||||
//
|
||||
// Depends on the three schema migrations above. Written for PocketBase v0.22+/v0.23.
|
||||
migrate(
|
||||
(app) => {
|
||||
// organization
|
||||
let org = null
|
||||
try {
|
||||
org = app.findFirstRecordByFilter('organizations', 'name = "PilotVault"')
|
||||
} catch (_) {
|
||||
/* not found */
|
||||
}
|
||||
if (!org) {
|
||||
const oc = app.findCollectionByNameOrId('organizations')
|
||||
org = new Record(oc)
|
||||
org.set('name', 'PilotVault')
|
||||
app.save(org)
|
||||
}
|
||||
|
||||
const uc = app.findCollectionByNameOrId('users')
|
||||
|
||||
const ensure = (email, password, role, orgId) => {
|
||||
let u = null
|
||||
try {
|
||||
u = app.findAuthRecordByEmail('users', email)
|
||||
} catch (_) {
|
||||
/* not found */
|
||||
}
|
||||
if (u) {
|
||||
let dirty = false
|
||||
if (u.get('role') !== role) {
|
||||
u.set('role', role)
|
||||
dirty = true
|
||||
}
|
||||
if ((u.get('organization') || '') !== (orgId || '')) {
|
||||
u.set('organization', orgId || '')
|
||||
dirty = true
|
||||
}
|
||||
if (dirty) app.save(u)
|
||||
return
|
||||
}
|
||||
u = new Record(uc)
|
||||
u.set('email', email)
|
||||
if (password) u.setPassword(password)
|
||||
u.set('role', role)
|
||||
u.set('verified', true)
|
||||
u.set('emailVisibility', false)
|
||||
if (orgId) u.set('organization', orgId)
|
||||
app.save(u)
|
||||
}
|
||||
|
||||
ensure('superadmin@pilotvault.local', 'pilotvaultsuperadmin2026!', 'superadmin', null)
|
||||
ensure('dariusz@pilotvault.local', null, 'admin', org.id)
|
||||
ensure('pilot@pilotvault.local', 'pilotvaultuser2026!', 'user', org.id)
|
||||
// pilot@dji.local is intentionally left as an org-less user.
|
||||
},
|
||||
(app) => {
|
||||
// Best-effort revert: drop the two seeded PilotVault accounts and the org.
|
||||
// dariusz@pilotvault.local / pilot@dji.local predate this seed and are kept.
|
||||
const drop = (email) => {
|
||||
try {
|
||||
app.delete(app.findAuthRecordByEmail('users', email))
|
||||
} catch (_) {
|
||||
/* already gone */
|
||||
}
|
||||
}
|
||||
drop('superadmin@pilotvault.local')
|
||||
drop('pilot@pilotvault.local')
|
||||
try {
|
||||
app.delete(app.findFirstRecordByFilter('organizations', 'name = "PilotVault"'))
|
||||
} catch (_) {
|
||||
/* already gone */
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// Adds a `pluginSettings` JSON field to both the `users` auth collection and the
|
||||
// `organizations` collection. It backs the per-user / per-organization layers of
|
||||
// the OpenSky plugin's cascading settings (API Server → Org Admin → User):
|
||||
// { "opensky": { "config": { clientId, clientSecret, plan, bbox }, "enabled": bool } }
|
||||
// The `enabled` flag is only meaningful on `users` (enablement is strictly per-user).
|
||||
//
|
||||
// Apply by copying this file into your PocketBase deployment's `pb_migrations/`
|
||||
// directory and restarting PocketBase (migrations run automatically on boot).
|
||||
// Idempotent: skips a collection whose field is already present. Written for
|
||||
// PocketBase v0.22+/v0.23 (JSVM `migrate((app) => …)` API).
|
||||
migrate(
|
||||
(app) => {
|
||||
for (const name of ['users', 'organizations']) {
|
||||
const col = app.findCollectionByNameOrId(name)
|
||||
if (col.fields.getByName('pluginSettings')) continue
|
||||
|
||||
col.fields.add(
|
||||
new Field({
|
||||
name: 'pluginSettings',
|
||||
type: 'json',
|
||||
required: false,
|
||||
presentable: false,
|
||||
maxSize: 100000, // small JSON blob of plugin config
|
||||
}),
|
||||
)
|
||||
app.save(col)
|
||||
}
|
||||
},
|
||||
(app) => {
|
||||
for (const name of ['users', 'organizations']) {
|
||||
const col = app.findCollectionByNameOrId(name)
|
||||
col.fields.removeByName('pluginSettings')
|
||||
app.save(col)
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user