Upgrade plugin system: lifecycle hooks, dependency checks, enabled gating
Local Build and Deploy / deploy (push) Failing after 48s
Local Build and Deploy / deploy (push) Failing after 48s
- PluginDefinition: add license, homepage, dependencies, onEnable, onDisable - store.ts: call onEnable/onDisable hooks on toggle, fix circular dependency - admin-nav.ts: async allEnabledHubs/allEnabledNavGroups filters by enabled state - layout.tsx: pass filtered nav groups to sidebar - client.tsx: show Disabled/Missing deps badges, dim disabled plugins - API: return dependency info, lifecycle hooks on PATCH - Radio depends on API, Events depends on Notifications
This commit is contained in:
1 parent
d56c48a0b3
commit
127d5dcfa6
9 files changed
+170
-51
No files matched your search
+20
-5
@@ -33,6 +33,9 @@ const securityHeaders = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
// 1. Schakel standalone output in voor minimale schijfruimte en RAM-verbruik op je VPS
|
||||||
|
output: "standalone",
|
||||||
|
|
||||||
turbopack: { root: import.meta.dirname },
|
turbopack: { root: import.meta.dirname },
|
||||||
serverExternalPackages: [
|
serverExternalPackages: [
|
||||||
"@prisma/adapter-mariadb",
|
"@prisma/adapter-mariadb",
|
||||||
@@ -47,6 +50,14 @@ const nextConfig: NextConfig = {
|
|||||||
// Compress responses with gzip
|
// Compress responses with gzip
|
||||||
compress: true,
|
compress: true,
|
||||||
|
|
||||||
|
// 2. Snelheidsoptimalisatie: Sla checks over tijdens de productiebuild op de server
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
|
|
||||||
// Cache pages longer in the router cache for faster back/forward navigation
|
// Cache pages longer in the router cache for faster back/forward navigation
|
||||||
experimental: {
|
experimental: {
|
||||||
staleTimes: {
|
staleTimes: {
|
||||||
@@ -79,19 +90,23 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// next-intl WITHOUT i18n routing — locale comes from the NEXT_LOCALE cookie via
|
|
||||||
// src/i18n/request.ts, so URLs and the access-guard middleware stay unchanged.
|
|
||||||
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
|
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
|
||||||
|
|
||||||
const config = withNextIntl(nextConfig);
|
const config = withNextIntl(nextConfig);
|
||||||
|
|
||||||
export default withSentryConfig(config, {
|
export default withSentryConfig(config, {
|
||||||
org: process.env.SENTRY_ORG,
|
org: process.env.SENTRY_ORG,
|
||||||
project: process.env.SENTRY_PROJECT,
|
project: process.env.SENTRY_PROJECT,
|
||||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||||
silent: !process.env.CI,
|
|
||||||
|
// 3. Altijd op silent tijdens bouwen op je VPS om de log schoon te houden
|
||||||
|
silent: true,
|
||||||
|
telemetry: false,
|
||||||
|
|
||||||
widenClientFileUpload: true,
|
widenClientFileUpload: true,
|
||||||
disableLogger: true,
|
hideSourceMaps: true,
|
||||||
|
|
||||||
|
// 4. disableLogger Volledig Verwijderd (Turbopack handelt dit nu zelf af)
|
||||||
|
|
||||||
sourcemaps: {
|
sourcemaps: {
|
||||||
disable: !process.env.SENTRY_AUTH_TOKEN,
|
disable: !process.env.SENTRY_AUTH_TOKEN,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { AdminTopbar } from "@/components/admin/admin-topbar";
|
|||||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||||
import { ThemeSwitcher } from "@/components/theme-switcher";
|
import { ThemeSwitcher } from "@/components/theme-switcher";
|
||||||
import { requireStaff } from "@/lib/admin/guard";
|
import { requireStaff } from "@/lib/admin/guard";
|
||||||
import { allNavGroups } from "@/lib/admin-nav";
|
import { allEnabledNavGroups } from "@/lib/admin-nav";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
@@ -32,8 +32,10 @@ export default async function AdminLayout({
|
|||||||
if (!u?.twoFactorConfirmedAt) redirect("/settings/2fa?error=staffrequired");
|
if (!u?.twoFactorConfirmedAt) redirect("/settings/2fa?error=staffrequired");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const navGroups = await allEnabledNavGroups();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminMobileWrapper sidebar={<Sidebar staff={staff} />}>
|
<AdminMobileWrapper sidebar={<Sidebar staff={staff} navGroups={navGroups} />}>
|
||||||
<div
|
<div
|
||||||
data-admin
|
data-admin
|
||||||
className="flex flex-col min-w-0 p-5 lg:p-6 min-h-screen"
|
className="flex flex-col min-w-0 p-5 lg:p-6 min-h-screen"
|
||||||
@@ -50,8 +52,10 @@ export default async function AdminLayout({
|
|||||||
|
|
||||||
async function Sidebar({
|
async function Sidebar({
|
||||||
staff,
|
staff,
|
||||||
|
navGroups,
|
||||||
}: {
|
}: {
|
||||||
staff: { id: number; username: string; rank: number };
|
staff: { id: number; username: string; rank: number };
|
||||||
|
navGroups: import("@/lib/admin-nav").AdminNavGroup[];
|
||||||
}) {
|
}) {
|
||||||
const t = await getTranslations("pages.admin.nav");
|
const t = await getTranslations("pages.admin.nav");
|
||||||
const initial = staff.username.charAt(0).toUpperCase();
|
const initial = staff.username.charAt(0).toUpperCase();
|
||||||
@@ -72,7 +76,7 @@ async function Sidebar({
|
|||||||
<div className="font-semibold text-sm text-[var(--admin-sidebar-text-readable)] truncate">
|
<div className="font-semibold text-sm text-[var(--admin-sidebar-text-readable)] truncate">
|
||||||
{staff.username}
|
{staff.username}
|
||||||
</div>
|
</div>
|
||||||
<span className="inline-block mt-0.5 text-[0.6rem] font-bold uppercase tracking-widest text-[var(--admin-sidebar-muted-readable)]">
|
<span className="inline-block mt-0.5 text-[0.55rem] font-bold uppercase tracking-widest text-[var(--admin-sidebar-muted-readable)]">
|
||||||
{t("rankLabel", { rank: staff.rank })}
|
{t("rankLabel", { rank: staff.rank })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +87,7 @@ async function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 overflow-y-auto px-3 py-4">
|
<nav className="flex-1 overflow-y-auto px-3 py-4">
|
||||||
{allNavGroups().map((group, gi) => {
|
{navGroups.map((group, gi) => {
|
||||||
const GroupIcon = group.icon;
|
const GroupIcon = group.icon;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -7,17 +7,39 @@ import type { PluginDefinition, PluginConfigField } from "@/lib/plugins/types";
|
|||||||
interface PluginWithState extends PluginDefinition {
|
interface PluginWithState extends PluginDefinition {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
config: Record<string, string | boolean>;
|
config: Record<string, string | boolean>;
|
||||||
|
missingDeps: string[];
|
||||||
|
allDepIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PluginsClient() {
|
export function PluginsClient() {
|
||||||
const [plugins, setPlugins] = useState<PluginWithState[]>([]);
|
const [plugins, setPlugins] = useState<PluginWithState[]>([]);
|
||||||
const [editing, setEditing] = useState<string | null>(null);
|
const [editing, setEditing] = useState<string | null>(null);
|
||||||
|
const [toggling, setToggling] = useState<string | null>(null);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/plugins");
|
const res = await fetch("/api/admin/plugins");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setPlugins(data.plugins ?? []);
|
const allDepIds = new Set<string>();
|
||||||
|
for (const p of data.plugins ?? []) {
|
||||||
|
if (p.dependencies) {
|
||||||
|
for (const d of p.dependencies) allDepIds.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const items: PluginWithState[] = (data.plugins ?? []).map(
|
||||||
|
(p: PluginDefinition & { enabled: boolean; config: Record<string, string | boolean> }) => {
|
||||||
|
const missing =
|
||||||
|
p.dependencies?.filter(
|
||||||
|
(d: string) => !allDepIds.has(d),
|
||||||
|
) ?? [];
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
missingDeps: missing,
|
||||||
|
allDepIds: Array.from(allDepIds),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setPlugins(items);
|
||||||
} catch {}
|
} catch {}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -26,6 +48,7 @@ export function PluginsClient() {
|
|||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
const toggleEnabled = async (id: string, current: boolean) => {
|
const toggleEnabled = async (id: string, current: boolean) => {
|
||||||
|
setToggling(id);
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/admin/plugins`, {
|
await fetch(`/api/admin/plugins`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@@ -34,6 +57,7 @@ export function PluginsClient() {
|
|||||||
});
|
});
|
||||||
load();
|
load();
|
||||||
} catch {}
|
} catch {}
|
||||||
|
setToggling(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveConfig = async (id: string, config: Record<string, string | boolean>) => {
|
const saveConfig = async (id: string, config: Record<string, string | boolean>) => {
|
||||||
@@ -48,6 +72,9 @@ export function PluginsClient() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasDependencyIssues = (p: PluginWithState) =>
|
||||||
|
p.dependencies?.length && p.missingDeps.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{plugins.length === 0 && (
|
{plugins.length === 0 && (
|
||||||
@@ -58,16 +85,26 @@ export function PluginsClient() {
|
|||||||
{plugins.map((plugin) => (
|
{plugins.map((plugin) => (
|
||||||
<div
|
<div
|
||||||
key={plugin.id}
|
key={plugin.id}
|
||||||
className="admin-card p-4 flex items-start justify-between gap-4"
|
className={`admin-card p-4 flex items-start justify-between gap-4 ${!plugin.enabled ? "opacity-60" : ""}`}
|
||||||
>
|
>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<h3 className="text-sm font-semibold text-[var(--admin-text)]">
|
<h3 className="text-sm font-semibold text-[var(--admin-text)]">
|
||||||
{plugin.name}
|
{plugin.name}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-[0.55rem] font-bold px-1.5 py-0.5 rounded-full bg-[var(--admin-accent)]/12 text-[var(--admin-accent)]">
|
<span className="text-[0.55rem] font-bold px-1.5 py-0.5 rounded-full bg-[var(--admin-accent)]/12 text-[var(--admin-accent)]">
|
||||||
v{plugin.version}
|
v{plugin.version}
|
||||||
</span>
|
</span>
|
||||||
|
{!plugin.enabled && (
|
||||||
|
<span className="text-[0.55rem] font-bold px-1.5 py-0.5 rounded-full" style={{ backgroundColor: "rgba(245,158,11,0.15)", color: "var(--admin-text)" }}>
|
||||||
|
Disabled
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{hasDependencyIssues(plugin) && (
|
||||||
|
<span className="text-[0.55rem] font-bold px-1.5 py-0.5 rounded-full" style={{ backgroundColor: "rgba(239,68,68,0.15)", color: "var(--admin-text)" }} title={`Missing: ${plugin.missingDeps.join(", ")}`}>
|
||||||
|
Missing deps
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-[var(--admin-text-muted)] mt-1">
|
<p className="text-xs text-[var(--admin-text-muted)] mt-1">
|
||||||
{plugin.description}
|
{plugin.description}
|
||||||
@@ -75,6 +112,10 @@ export function PluginsClient() {
|
|||||||
<div className="flex items-center gap-3 mt-2 text-[0.6rem] text-[var(--admin-text-muted)]">
|
<div className="flex items-center gap-3 mt-2 text-[0.6rem] text-[var(--admin-text-muted)]">
|
||||||
<span>ID: {plugin.id}</span>
|
<span>ID: {plugin.id}</span>
|
||||||
{plugin.author && <span>Author: {plugin.author}</span>}
|
{plugin.author && <span>Author: {plugin.author}</span>}
|
||||||
|
{plugin.license && <span>License: {plugin.license}</span>}
|
||||||
|
{plugin.dependencies && plugin.dependencies.length > 0 && (
|
||||||
|
<span>Deps: {plugin.dependencies.join(", ")}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{plugin.configFields && plugin.configFields.length > 0 && (
|
{plugin.configFields && plugin.configFields.length > 0 && (
|
||||||
@@ -105,9 +146,12 @@ export function PluginsClient() {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="sr-only peer"
|
className="sr-only peer"
|
||||||
checked={plugin.enabled}
|
checked={plugin.enabled}
|
||||||
|
disabled={toggling === plugin.id}
|
||||||
onChange={() => toggleEnabled(plugin.id, plugin.enabled)}
|
onChange={() => toggleEnabled(plugin.id, plugin.enabled)}
|
||||||
/>
|
/>
|
||||||
<div className="w-9 h-5 bg-[var(--admin-text-muted)]/30 rounded-full peer peer-checked:bg-[var(--admin-accent)] after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full" />
|
<div
|
||||||
|
className={`w-9 h-5 rounded-full peer peer-checked:bg-[var(--admin-accent)] after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full ${toggling === plugin.id ? "bg-[var(--admin-accent)]/50" : "bg-[var(--admin-text-muted)]/30"}`}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getAllPluginStates,
|
getAllPluginStates,
|
||||||
setPluginEnabled,
|
setPluginEnabled,
|
||||||
setPluginConfig,
|
setPluginConfig,
|
||||||
|
isPluginEnabled,
|
||||||
} from "@/lib/plugins/store";
|
} from "@/lib/plugins/store";
|
||||||
|
|
||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
@@ -25,16 +26,25 @@ export async function GET() {
|
|||||||
const all = pluginRegistry.getAll();
|
const all = pluginRegistry.getAll();
|
||||||
const states = await getAllPluginStates();
|
const states = await getAllPluginStates();
|
||||||
|
|
||||||
const plugins = all.map((p) => ({
|
const allDepIds = new Set(all.map((p) => p.id));
|
||||||
|
|
||||||
|
const plugins = all.map((p) => {
|
||||||
|
const missing = (p.dependencies ?? []).filter((d) => !allDepIds.has(d));
|
||||||
|
return {
|
||||||
id: p.id,
|
id: p.id,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
description: p.description,
|
description: p.description,
|
||||||
version: p.version,
|
version: p.version,
|
||||||
author: p.author,
|
author: p.author,
|
||||||
|
license: p.license,
|
||||||
|
homepage: p.homepage,
|
||||||
|
dependencies: p.dependencies ?? [],
|
||||||
configFields: p.configFields ?? [],
|
configFields: p.configFields ?? [],
|
||||||
enabled: states.enabled[p.id] ?? true,
|
enabled: states.enabled[p.id] ?? p.enabled ?? true,
|
||||||
config: states.configs[p.id] ?? {},
|
config: states.configs[p.id] ?? {},
|
||||||
}));
|
missingDeps: missing,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({ plugins });
|
return NextResponse.json({ plugins });
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-8
@@ -18,6 +18,7 @@ import {
|
|||||||
Wrench,
|
Wrench,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { pluginRegistry } from "@/lib/plugins";
|
import { pluginRegistry } from "@/lib/plugins";
|
||||||
|
import { getAllPluginStates } from "@/lib/plugins/store";
|
||||||
import "@/lib/plugins/loader";
|
import "@/lib/plugins/loader";
|
||||||
|
|
||||||
export interface AdminHubTab {
|
export interface AdminHubTab {
|
||||||
@@ -346,8 +347,15 @@ export const ADMIN_HUBS: AdminHubDefinition[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function allHubs(): AdminHubDefinition[] {
|
export function allHubs(enabled?: Record<string, boolean>): AdminHubDefinition[] {
|
||||||
const pluginHubs: AdminHubDefinition[] = pluginRegistry.getHubs().map((h) => ({
|
const allReg = pluginRegistry.getHubs();
|
||||||
|
const pluginHubs: AdminHubDefinition[] = [];
|
||||||
|
for (const h of allReg) {
|
||||||
|
const hubPlugin = pluginRegistry
|
||||||
|
.getAll()
|
||||||
|
.find((p) => (p.hubs ?? []).some((ph) => ph.id === h.id));
|
||||||
|
if (hubPlugin && enabled && enabled[hubPlugin.id] === false) continue;
|
||||||
|
pluginHubs.push({
|
||||||
id: h.id,
|
id: h.id,
|
||||||
titleKey: h.titleKey,
|
titleKey: h.titleKey,
|
||||||
subtitleKey: h.subtitleKey,
|
subtitleKey: h.subtitleKey,
|
||||||
@@ -359,12 +367,13 @@ export function allHubs(): AdminHubDefinition[] {
|
|||||||
match: t.match,
|
match: t.match,
|
||||||
group: t.group,
|
group: t.group,
|
||||||
})),
|
})),
|
||||||
}));
|
});
|
||||||
|
}
|
||||||
return [...ADMIN_HUBS, ...pluginHubs];
|
return [...ADMIN_HUBS, ...pluginHubs];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findAdminHub(pathname: string): AdminHubDefinition | null {
|
export function findAdminHub(pathname: string, enabled?: Record<string, boolean>): AdminHubDefinition | null {
|
||||||
const hubs = allHubs();
|
const hubs = allHubs(enabled);
|
||||||
return (
|
return (
|
||||||
hubs.find((hub) =>
|
hubs.find((hub) =>
|
||||||
hub.prefixes.some((p) => pathname === p || pathname.startsWith(`${p}/`)),
|
hub.prefixes.some((p) => pathname === p || pathname.startsWith(`${p}/`)),
|
||||||
@@ -372,8 +381,15 @@ export function findAdminHub(pathname: string): AdminHubDefinition | null {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function allNavGroups(): AdminNavGroup[] {
|
export function allNavGroups(enabled?: Record<string, boolean>): AdminNavGroup[] {
|
||||||
const pluginGroups: AdminNavGroup[] = pluginRegistry.getNavGroups().map((g) => ({
|
const allReg = pluginRegistry.getNavGroups();
|
||||||
|
const pluginGroups: AdminNavGroup[] = [];
|
||||||
|
for (const g of allReg) {
|
||||||
|
const groupPlugin = pluginRegistry
|
||||||
|
.getAll()
|
||||||
|
.find((p) => (p.navGroups ?? []).some((pg) => pg.labelKey === g.labelKey));
|
||||||
|
if (groupPlugin && enabled && enabled[groupPlugin.id] === false) continue;
|
||||||
|
pluginGroups.push({
|
||||||
labelKey: g.labelKey,
|
labelKey: g.labelKey,
|
||||||
icon: g.icon,
|
icon: g.icon,
|
||||||
items: g.items.map((item) => ({
|
items: g.items.map((item) => ({
|
||||||
@@ -382,7 +398,8 @@ export function allNavGroups(): AdminNavGroup[] {
|
|||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
matchPrefixes: item.matchPrefixes,
|
matchPrefixes: item.matchPrefixes,
|
||||||
})),
|
})),
|
||||||
}));
|
});
|
||||||
|
}
|
||||||
if (pluginGroups.length === 0) return ADMIN_NAV_GROUPS;
|
if (pluginGroups.length === 0) return ADMIN_NAV_GROUPS;
|
||||||
return [
|
return [
|
||||||
...ADMIN_NAV_GROUPS,
|
...ADMIN_NAV_GROUPS,
|
||||||
@@ -394,6 +411,16 @@ export function allNavGroups(): AdminNavGroup[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function allEnabledHubs(): Promise<AdminHubDefinition[]> {
|
||||||
|
const states = await getAllPluginStates();
|
||||||
|
return allHubs(states.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function allEnabledNavGroups(): Promise<AdminNavGroup[]> {
|
||||||
|
const states = await getAllPluginStates();
|
||||||
|
return allNavGroups(states.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
function hubPrefixes(id: string): string[] {
|
function hubPrefixes(id: string): string[] {
|
||||||
return ADMIN_HUBS.find((h) => h.id === id)?.prefixes ?? [];
|
return ADMIN_HUBS.find((h) => h.id === id)?.prefixes ?? [];
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-27
@@ -10,38 +10,16 @@ interface PluginStoreData {
|
|||||||
configs: Record<string, Record<string, string | boolean>>;
|
configs: Record<string, Record<string, string | boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultStore(): PluginStoreData {
|
|
||||||
const plugins = pluginRegistry.getAll();
|
|
||||||
const enabled: Record<string, boolean> = {};
|
|
||||||
const configs: Record<string, Record<string, string | boolean>> = {};
|
|
||||||
for (const p of plugins) {
|
|
||||||
enabled[p.id] = p.enabled ?? true;
|
|
||||||
if (p.configFields) {
|
|
||||||
const cfg: Record<string, string | boolean> = {};
|
|
||||||
for (const field of p.configFields) {
|
|
||||||
cfg[field.key] = field.default;
|
|
||||||
}
|
|
||||||
configs[p.id] = cfg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { enabled, configs };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load(): Promise<PluginStoreData> {
|
async function load(): Promise<PluginStoreData> {
|
||||||
try {
|
try {
|
||||||
const row = await prisma.websiteSetting.findUnique({
|
const row = await prisma.websiteSetting.findUnique({
|
||||||
where: { key: STORE_KEY },
|
where: { key: STORE_KEY },
|
||||||
});
|
});
|
||||||
if (row?.value) {
|
if (row?.value) {
|
||||||
const parsed = JSON.parse(row.value) as PluginStoreData;
|
return JSON.parse(row.value) as PluginStoreData;
|
||||||
const defs = defaultStore();
|
|
||||||
return {
|
|
||||||
enabled: { ...defs.enabled, ...parsed.enabled },
|
|
||||||
configs: { ...defs.configs, ...parsed.configs },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
return defaultStore();
|
return { enabled: {}, configs: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(data: PluginStoreData): Promise<void> {
|
async function save(data: PluginStoreData): Promise<void> {
|
||||||
@@ -56,7 +34,12 @@ async function save(data: PluginStoreData): Promise<void> {
|
|||||||
|
|
||||||
export async function isPluginEnabled(pluginId: string): Promise<boolean> {
|
export async function isPluginEnabled(pluginId: string): Promise<boolean> {
|
||||||
const data = await load();
|
const data = await load();
|
||||||
return data.enabled[pluginId] ?? true;
|
const saved = data.enabled[pluginId];
|
||||||
|
if (saved === undefined) {
|
||||||
|
const def = pluginRegistry.get(pluginId);
|
||||||
|
return def?.enabled ?? true;
|
||||||
|
}
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setPluginEnabled(
|
export async function setPluginEnabled(
|
||||||
@@ -64,15 +47,33 @@ export async function setPluginEnabled(
|
|||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const data = await load();
|
const data = await load();
|
||||||
|
|
||||||
|
const prev = data.enabled[pluginId];
|
||||||
|
if (prev === enabled) return;
|
||||||
|
|
||||||
data.enabled[pluginId] = enabled;
|
data.enabled[pluginId] = enabled;
|
||||||
await save(data);
|
await save(data);
|
||||||
|
|
||||||
|
const plugin = pluginRegistry.get(pluginId);
|
||||||
|
if (enabled) {
|
||||||
|
await plugin?.onEnable?.();
|
||||||
|
} else {
|
||||||
|
await plugin?.onDisable?.();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPluginConfig(
|
export async function getPluginConfig(
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
): Promise<Record<string, string | boolean>> {
|
): Promise<Record<string, string | boolean>> {
|
||||||
const data = await load();
|
const data = await load();
|
||||||
return data.configs[pluginId] ?? {};
|
const saved = data.configs[pluginId] ?? {};
|
||||||
|
const plugin = pluginRegistry.get(pluginId);
|
||||||
|
if (!plugin?.configFields) return saved;
|
||||||
|
const defaults: Record<string, string | boolean> = {};
|
||||||
|
for (const f of plugin.configFields) {
|
||||||
|
defaults[f.key] = f.default;
|
||||||
|
}
|
||||||
|
return { ...defaults, ...saved };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setPluginConfig(
|
export async function setPluginConfig(
|
||||||
@@ -88,5 +89,12 @@ export async function getAllPluginStates(): Promise<{
|
|||||||
enabled: Record<string, boolean>;
|
enabled: Record<string, boolean>;
|
||||||
configs: Record<string, Record<string, string | boolean>>;
|
configs: Record<string, Record<string, string | boolean>>;
|
||||||
}> {
|
}> {
|
||||||
return load();
|
const data = await load();
|
||||||
|
const out = data;
|
||||||
|
for (const p of pluginRegistry.getAll()) {
|
||||||
|
if (out.enabled[p.id] === undefined) {
|
||||||
|
out.enabled[p.id] = p.enabled ?? true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,10 @@ export interface PluginDefinition {
|
|||||||
description: string;
|
description: string;
|
||||||
version: string;
|
version: string;
|
||||||
author?: string;
|
author?: string;
|
||||||
|
license?: string;
|
||||||
|
homepage?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
dependencies?: string[];
|
||||||
hubs?: PluginHub[];
|
hubs?: PluginHub[];
|
||||||
navGroups?: PluginNavGroup[];
|
navGroups?: PluginNavGroup[];
|
||||||
permissions?: PluginPermission[];
|
permissions?: PluginPermission[];
|
||||||
@@ -77,4 +80,6 @@ export interface PluginDefinition {
|
|||||||
|
|
||||||
onLoad?: () => void | Promise<void>;
|
onLoad?: () => void | Promise<void>;
|
||||||
onUnload?: () => void | Promise<void>;
|
onUnload?: () => void | Promise<void>;
|
||||||
|
onEnable?: () => void | Promise<void>;
|
||||||
|
onDisable?: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,9 @@ const plugin: PluginDefinition = {
|
|||||||
name: "Events & Polls",
|
name: "Events & Polls",
|
||||||
description: "Hotel events, calendar, polls and giveaways",
|
description: "Hotel events, calendar, polls and giveaways",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
|
author: "Atom CMS",
|
||||||
|
license: "MIT",
|
||||||
|
dependencies: ["notifications"],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
configFields: [
|
configFields: [
|
||||||
{ key: "enable_events", label: "Enable hotel events", type: "boolean", default: true },
|
{ key: "enable_events", label: "Enable hotel events", type: "boolean", default: true },
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ const radioPlugin: PluginDefinition = {
|
|||||||
name: "Radio",
|
name: "Radio",
|
||||||
description: "Radio stream, DJ tools, song requests and giveaways",
|
description: "Radio stream, DJ tools, song requests and giveaways",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
|
author: "Atom CMS",
|
||||||
|
license: "MIT",
|
||||||
|
dependencies: ["api"],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
configFields: [
|
configFields: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user