mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
feat(plugins): plugin system scaffold — registry tables + admin panel
First slice of the plugin system. Lays down the data model and a read-only admin surface; nothing executes yet. - Migration 155: plugins, plugin_meta_migrations, plugin_error_log and plugin_settings_fields tables. Plugin data will live in a per-plugin sqlite file under /plugins-data, never in these tables. - New Nest module server/src/nest/plugins with GET /api/admin/plugins (admin-gated, returns the installed list + the runtime-enabled flag). - TREK_PLUGINS_ENABLED kill switch (config.pluginsEnabled), off by default. - Admin → Plugins tab with a read-only panel: installed list, status badges, and a clear banner when the runtime is disabled by server config. - i18n keys for the tab and panel across all locales. Install, activation, the isolated runtime and the registry browser follow in later slices.
This commit is contained in:
@@ -174,3 +174,14 @@ export const SESSION_DURATION_REMEMBER =
|
||||
export const SESSION_DURATION_REMEMBER_MS = parsedRememberMs ?? parseDurationMs(DEFAULT_SESSION_DURATION_REMEMBER)!;
|
||||
/** "Remember me" session length in seconds — passed to `jwt.sign({ expiresIn })`. */
|
||||
export const SESSION_DURATION_REMEMBER_SECONDS = Math.floor(SESSION_DURATION_REMEMBER_MS / 1000);
|
||||
|
||||
/**
|
||||
* Plugin-system kill switch (#plugins). Off by default — a brand-new, high-risk
|
||||
* surface an instance owner must deliberately opt into. When false, the plugin
|
||||
* module serves its admin listing (so the tab isn't a dead end) but never spawns
|
||||
* a plugin process and refuses install/activate. Read at call time so tests and
|
||||
* runtime env changes take effect without re-import.
|
||||
*/
|
||||
export function pluginsEnabled(): boolean {
|
||||
return (process.env.TREK_PLUGINS_ENABLED || '').trim().toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
@@ -3289,6 +3289,69 @@ function runMigrations(db: Database.Database): void {
|
||||
() => {
|
||||
try { db.exec('ALTER TABLE invite_tokens ADD COLUMN trip_id INTEGER REFERENCES trips(id) ON DELETE SET NULL'); } catch (err) { console.warn('[migrations] Non-fatal migration step failed:', err); }
|
||||
},
|
||||
// Migration 155: plugin system scaffold (#plugins). A plugin is a row here;
|
||||
// its code lives on the /plugins volume and (once the runtime lands) runs in
|
||||
// an isolated child process. This migration only lays down the registry
|
||||
// tables — nothing executes yet. Own data lives in a per-plugin sqlite file
|
||||
// under /plugins-data, never in these tables.
|
||||
() => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS plugins (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
type TEXT NOT NULL DEFAULT 'integration',
|
||||
icon TEXT DEFAULT 'Blocks',
|
||||
version TEXT,
|
||||
api_version INTEGER DEFAULT 1,
|
||||
min_trek_version TEXT,
|
||||
permissions TEXT DEFAULT '[]',
|
||||
granted_permissions TEXT DEFAULT '[]',
|
||||
status TEXT NOT NULL DEFAULT 'inactive',
|
||||
config TEXT DEFAULT '{}',
|
||||
source_repo TEXT,
|
||||
source_commit TEXT,
|
||||
sha256 TEXT,
|
||||
crash_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
reviewed_at TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
installed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS plugin_meta_migrations (
|
||||
plugin_id TEXT NOT NULL,
|
||||
migration_id TEXT NOT NULL,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (plugin_id, migration_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS plugin_error_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plugin_id TEXT NOT NULL,
|
||||
ts DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
level TEXT NOT NULL DEFAULT 'error',
|
||||
message TEXT,
|
||||
stack TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_plugin_error_log_plugin ON plugin_error_log(plugin_id, ts);
|
||||
CREATE TABLE IF NOT EXISTS plugin_settings_fields (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plugin_id TEXT NOT NULL,
|
||||
field_key TEXT NOT NULL,
|
||||
label TEXT,
|
||||
input_type TEXT NOT NULL DEFAULT 'text',
|
||||
placeholder TEXT,
|
||||
hint TEXT,
|
||||
required INTEGER NOT NULL DEFAULT 0,
|
||||
secret INTEGER NOT NULL DEFAULT 0,
|
||||
scope TEXT NOT NULL DEFAULT 'instance',
|
||||
options TEXT,
|
||||
oauth_config TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE (plugin_id, field_key)
|
||||
);
|
||||
`);
|
||||
},
|
||||
];
|
||||
|
||||
if (currentVersion < migrations.length) {
|
||||
|
||||
@@ -41,6 +41,7 @@ import { OidcModule } from './oidc/oidc.module';
|
||||
import { OauthModule } from './oauth/oauth.module';
|
||||
import { AdminModule } from './admin/admin.module';
|
||||
import { AddonsModule } from './addons/addons.module';
|
||||
import { PluginsModule } from './plugins/plugins.module';
|
||||
import { TrekExceptionFilter } from './common/trek-exception.filter';
|
||||
import { SpaFallbackFilter } from './platform/spa-fallback.filter';
|
||||
import { IdempotencyInterceptor } from './common/idempotency.interceptor';
|
||||
@@ -51,7 +52,7 @@ import { IdempotencyInterceptor } from './common/idempotency.interceptor';
|
||||
* migrated.
|
||||
*/
|
||||
@Module({
|
||||
imports: [DatabaseModule, WeatherModule, HelpModule, AirportsModule, ConfigModule, SystemNoticesModule, MapsModule, CategoriesModule, TagsModule, NotificationsModule, AtlasModule, VacayModule, PackingModule, TodoModule, BudgetModule, ReservationsModule, DaysModule, AssignmentsModule, PlacesModule, TripsModule, CollabModule, FilesModule, PhotosModule, MemoriesModule, AirtrailModule, JourneyModule, CollectionsModule, ShareModule, TripInviteModule, TransitModule, FeedsModule, SettingsModule, BackupModule, AuthModule, OidcModule, OauthModule, AdminModule, AddonsModule, BookingImportModule],
|
||||
imports: [DatabaseModule, WeatherModule, HelpModule, AirportsModule, ConfigModule, SystemNoticesModule, MapsModule, CategoriesModule, TagsModule, NotificationsModule, AtlasModule, VacayModule, PackingModule, TodoModule, BudgetModule, ReservationsModule, DaysModule, AssignmentsModule, PlacesModule, TripsModule, CollabModule, FilesModule, PhotosModule, MemoriesModule, AirtrailModule, JourneyModule, CollectionsModule, ShareModule, TripInviteModule, TransitModule, FeedsModule, SettingsModule, BackupModule, AuthModule, OidcModule, OauthModule, AdminModule, AddonsModule, PluginsModule, BookingImportModule],
|
||||
controllers: [HealthController],
|
||||
providers: [
|
||||
HealthService,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { PluginsService } from './plugins.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { AdminGuard } from '../auth/admin.guard';
|
||||
|
||||
/**
|
||||
* /api/admin/plugins — admin-only plugin control surface (#plugins).
|
||||
*
|
||||
* M0 scaffold: read-only listing + the runtime-enabled flag. Install, activate,
|
||||
* disable, uninstall and the registry browser are added in later milestones.
|
||||
* Admin-gated like the rest of /api/admin. The proxy namespace /api/plugins/:id
|
||||
* is reserved for the (future) runtime and is intentionally NOT part of this
|
||||
* admin controller.
|
||||
*/
|
||||
@Controller('api/admin/plugins')
|
||||
@UseGuards(JwtAuthGuard, AdminGuard)
|
||||
export class PluginsController {
|
||||
constructor(private readonly plugins: PluginsService) {}
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.plugins.list();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PluginsController } from './plugins.controller';
|
||||
import { PluginsService } from './plugins.service';
|
||||
|
||||
/**
|
||||
* Plugin system (#plugins). M0 registers only the read side; the isolated
|
||||
* runtime supervisor, install pipeline and registry service are added in later
|
||||
* milestones under this same module.
|
||||
*/
|
||||
@Module({
|
||||
controllers: [PluginsController],
|
||||
providers: [PluginsService],
|
||||
})
|
||||
export class PluginsModule {}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { db } from '../../db/database';
|
||||
import { pluginsEnabled } from '../../config';
|
||||
|
||||
/**
|
||||
* Read side of the plugin system (#plugins), M0 scaffold. Lists installed
|
||||
* plugins from the `plugins` registry table and reports whether the runtime is
|
||||
* enabled (TREK_PLUGINS_ENABLED). No execution here — the isolated runtime,
|
||||
* install pipeline and registry fetch land in later milestones.
|
||||
*/
|
||||
|
||||
export interface PluginListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
type: string;
|
||||
icon: string | null;
|
||||
version: string | null;
|
||||
status: string;
|
||||
reviewed_at: string | null;
|
||||
source_repo: string | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PluginsService {
|
||||
list(): { enabled: boolean; plugins: PluginListItem[] } {
|
||||
const plugins = db
|
||||
.prepare(
|
||||
`SELECT id, name, description, type, icon, version, status, reviewed_at, source_repo
|
||||
FROM plugins
|
||||
ORDER BY sort_order, name`,
|
||||
)
|
||||
.all() as PluginListItem[];
|
||||
return { enabled: pluginsEnabled(), plugins };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user