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:
tajniak81
2026-07-13 11:43:33 +02:00
co-authored by Claude Opus 4.8
commit afc6952eda
172 changed files with 24591 additions and 0 deletions
@@ -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)
}
},
)