nexus base
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-05-15 01:19:31 +02:00
parent 52158ef041
commit 3ed4fba58c
12 changed files with 1975 additions and 29 deletions

View File

@@ -16,6 +16,7 @@ final class App
private ?\PDO $basePdo;
private ModuleManager $modules;
private AuthService $auth;
private NexusDashboardService $dashboards;
private function __construct(private Config $config)
{
@@ -43,6 +44,7 @@ final class App
$this->modules = new ModuleManager($this->basePdo, $modulesPath);
$this->modules->bootEnabled();
$this->auth = new AuthService($this);
$this->dashboards = new NexusDashboardService($this->basePdo);
}
public static function init(Config $config): self
@@ -71,4 +73,5 @@ final class App
public function basePdo(): ?\PDO { return $this->basePdo; }
public function modules(): ModuleManager { return $this->modules; }
public function auth(): AuthService { return $this->auth; }
public function dashboards(): NexusDashboardService { return $this->dashboards; }
}

View File

@@ -148,6 +148,88 @@ final class BaseSchema
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dashboards (
id BIGSERIAL PRIMARY KEY,
owner_key TEXT NOT NULL,
title TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT NULL,
visibility TEXT NOT NULL DEFAULT 'private',
sort_order INTEGER NOT NULL DEFAULT 0,
is_default BOOLEAN NOT NULL DEFAULT false,
config TEXT NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS nexus_dashboards_owner_slug_idx ON nexus_dashboards (owner_key, slug)");
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dashboard_items (
id BIGSERIAL PRIMARY KEY,
dashboard_id BIGINT NOT NULL,
item_type TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT NULL,
grid_column INTEGER NULL,
grid_row INTEGER NULL,
column_span INTEGER NOT NULL DEFAULT 1,
row_span INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 0,
config TEXT NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
$pdo->exec("CREATE INDEX IF NOT EXISTS nexus_dashboard_items_dashboard_idx ON nexus_dashboard_items (dashboard_id, sort_order, id)");
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_integrations (
id BIGSERIAL PRIMARY KEY,
owner_key TEXT NOT NULL,
type TEXT NOT NULL,
name TEXT NOT NULL,
base_url TEXT NULL,
visibility TEXT NOT NULL DEFAULT 'private',
is_active BOOLEAN NOT NULL DEFAULT true,
config TEXT NOT NULL DEFAULT '{}',
secrets TEXT NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_page_modules (
id BIGSERIAL PRIMARY KEY,
owner_key TEXT NOT NULL,
title TEXT NOT NULL,
slug TEXT NOT NULL,
module_type TEXT NOT NULL,
target_url TEXT NULL,
description TEXT NULL,
visibility TEXT NOT NULL DEFAULT 'private',
open_mode TEXT NOT NULL DEFAULT 'embed',
is_active BOOLEAN NOT NULL DEFAULT true,
config TEXT NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS nexus_page_modules_owner_slug_idx ON nexus_page_modules (owner_key, slug)");
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dashboard_shares (
id BIGSERIAL PRIMARY KEY,
dashboard_id BIGINT NOT NULL,
share_type TEXT NOT NULL,
share_value TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
$pdo->exec("CREATE INDEX IF NOT EXISTS nexus_dashboard_shares_dashboard_idx ON nexus_dashboard_shares (dashboard_id, share_type, share_value)");
}
private static function ensureSqlite(\PDO $pdo): void
@@ -276,6 +358,88 @@ final class BaseSchema
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dashboards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_key TEXT NOT NULL,
title TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT NULL,
visibility TEXT NOT NULL DEFAULT 'private',
sort_order INTEGER NOT NULL DEFAULT 0,
is_default INTEGER NOT NULL DEFAULT 0,
config TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)"
);
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS nexus_dashboards_owner_slug_idx ON nexus_dashboards (owner_key, slug)");
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dashboard_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dashboard_id INTEGER NOT NULL,
item_type TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT NULL,
grid_column INTEGER NULL,
grid_row INTEGER NULL,
column_span INTEGER NOT NULL DEFAULT 1,
row_span INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 0,
config TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)"
);
$pdo->exec("CREATE INDEX IF NOT EXISTS nexus_dashboard_items_dashboard_idx ON nexus_dashboard_items (dashboard_id, sort_order, id)");
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_integrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_key TEXT NOT NULL,
type TEXT NOT NULL,
name TEXT NOT NULL,
base_url TEXT NULL,
visibility TEXT NOT NULL DEFAULT 'private',
is_active INTEGER NOT NULL DEFAULT 1,
config TEXT NOT NULL DEFAULT '{}',
secrets TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_page_modules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_key TEXT NOT NULL,
title TEXT NOT NULL,
slug TEXT NOT NULL,
module_type TEXT NOT NULL,
target_url TEXT NULL,
description TEXT NULL,
visibility TEXT NOT NULL DEFAULT 'private',
open_mode TEXT NOT NULL DEFAULT 'embed',
is_active INTEGER NOT NULL DEFAULT 1,
config TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)"
);
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS nexus_page_modules_owner_slug_idx ON nexus_page_modules (owner_key, slug)");
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dashboard_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dashboard_id INTEGER NOT NULL,
share_type TEXT NOT NULL,
share_value TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)"
);
$pdo->exec("CREATE INDEX IF NOT EXISTS nexus_dashboard_shares_dashboard_idx ON nexus_dashboard_shares (dashboard_id, share_type, share_value)");
}
private static function ensureGeneric(\PDO $pdo): void
@@ -404,6 +568,88 @@ final class BaseSchema
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dashboards (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
owner_key VARCHAR(190) NOT NULL,
title VARCHAR(255) NOT NULL,
slug VARCHAR(190) NOT NULL,
description TEXT NULL,
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
sort_order INT NOT NULL DEFAULT 0,
is_default TINYINT NOT NULL DEFAULT 0,
config TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY nexus_dashboards_owner_slug_idx (owner_key, slug)
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dashboard_items (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
dashboard_id BIGINT NOT NULL,
item_type VARCHAR(64) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT NULL,
grid_column INT NULL,
grid_row INT NULL,
column_span INT NOT NULL DEFAULT 1,
row_span INT NOT NULL DEFAULT 1,
sort_order INT NOT NULL DEFAULT 0,
config TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY nexus_dashboard_items_dashboard_idx (dashboard_id, sort_order, id)
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_integrations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
owner_key VARCHAR(190) NOT NULL,
type VARCHAR(64) NOT NULL,
name VARCHAR(255) NOT NULL,
base_url TEXT NULL,
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
is_active TINYINT NOT NULL DEFAULT 1,
config TEXT NOT NULL,
secrets TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_page_modules (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
owner_key VARCHAR(190) NOT NULL,
title VARCHAR(255) NOT NULL,
slug VARCHAR(190) NOT NULL,
module_type VARCHAR(64) NOT NULL,
target_url TEXT NULL,
description TEXT NULL,
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
open_mode VARCHAR(32) NOT NULL DEFAULT 'embed',
is_active TINYINT NOT NULL DEFAULT 1,
config TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY nexus_page_modules_owner_slug_idx (owner_key, slug)
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_dashboard_shares (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
dashboard_id BIGINT NOT NULL,
share_type VARCHAR(32) NOT NULL,
share_value VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY nexus_dashboard_shares_dashboard_idx (dashboard_id, share_type, share_value)
)"
);
}
private static function seedTimezones(\PDO $pdo): void

View File

@@ -0,0 +1,628 @@
<?php
declare(strict_types=1);
namespace App;
final class NexusDashboardService
{
public function __construct(private ?\PDO $pdo)
{
}
public function available(): bool
{
return $this->pdo instanceof \PDO;
}
public function ensureDefaultDashboard(string $ownerKey, string $title = 'Mein Dashboard'): array
{
$dashboards = $this->listDashboardsForOwner($ownerKey);
if ($dashboards !== []) {
foreach ($dashboards as $dashboard) {
if (!empty($dashboard['is_default'])) {
return $dashboard;
}
}
$this->setDefaultDashboard($ownerKey, (int) $dashboards[0]['id']);
return $this->getDashboardById((int) $dashboards[0]['id']) ?? $dashboards[0];
}
$id = $this->createDashboard($ownerKey, [
'title' => $title,
'slug' => 'mein-dashboard',
'description' => 'Persönliches Nexus-Dashboard.',
'visibility' => 'private',
'is_default' => true,
]);
return $this->getDashboardById($id) ?? [];
}
public function listDashboardsForOwner(string $ownerKey): array
{
if (!$this->available() || $ownerKey === '') {
return [];
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_dashboards
WHERE owner_key = :owner
ORDER BY is_default DESC, sort_order ASC, title ASC, id ASC"
);
$stmt->execute(['owner' => $ownerKey]);
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
}
public function listAccessibleDashboards(string $ownerKey, array $groups = []): array
{
if (!$this->available()) {
return [];
}
$rows = [];
$seen = [];
foreach ($this->listDashboardsForOwner($ownerKey) as $dashboard) {
$id = (int) ($dashboard['id'] ?? 0);
if ($id > 0) {
$rows[] = $dashboard;
$seen[$id] = true;
}
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_dashboards
WHERE visibility = 'public'
ORDER BY sort_order ASC, title ASC, id ASC"
);
$stmt->execute();
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $dashboard) {
$id = (int) ($dashboard['id'] ?? 0);
if ($id > 0 && !isset($seen[$id])) {
$rows[] = $dashboard;
$seen[$id] = true;
}
}
if ($ownerKey !== '') {
$shared = $this->listSharedDashboardIds($ownerKey, $groups);
foreach ($shared as $dashboardId) {
if (isset($seen[$dashboardId])) {
continue;
}
$dashboard = $this->getDashboardById($dashboardId);
if ($dashboard !== null) {
$rows[] = $dashboard;
$seen[$dashboardId] = true;
}
}
}
usort($rows, static function (array $left, array $right): int {
return [$left['sort_order'] ?? 0, (string) ($left['title'] ?? ''), (int) ($left['id'] ?? 0)]
<=> [$right['sort_order'] ?? 0, (string) ($right['title'] ?? ''), (int) ($right['id'] ?? 0)];
});
return $rows;
}
public function getDashboardById(int $id): ?array
{
if (!$this->available() || $id <= 0) {
return null;
}
$stmt = $this->pdo->prepare("SELECT * FROM nexus_dashboards WHERE id = :id LIMIT 1");
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
return is_array($row) ? $this->hydrateRow($row) : null;
}
public function createDashboard(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
return 0;
}
$title = trim((string) ($data['title'] ?? 'Neues Dashboard'));
$slug = $this->uniqueSlug('nexus_dashboards', $ownerKey, (string) ($data['slug'] ?? $title), 0);
$description = trim((string) ($data['description'] ?? ''));
$visibility = $this->normalizeVisibility((string) ($data['visibility'] ?? 'private'));
$sortOrder = (int) ($data['sort_order'] ?? $this->nextSortOrder('nexus_dashboards', 'owner_key', $ownerKey));
$isDefault = !empty($data['is_default']);
$config = $this->encodeJson((array) ($data['config'] ?? []));
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_dashboards
(owner_key, title, slug, description, visibility, sort_order, is_default, config, created_at, updated_at)
VALUES
(:owner_key, :title, :slug, :description, :visibility, :sort_order, :is_default, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'owner_key' => $ownerKey,
'title' => $title !== '' ? $title : 'Neues Dashboard',
'slug' => $slug,
'description' => $description,
'visibility' => $visibility,
'sort_order' => $sortOrder,
'is_default' => $isDefault ? 1 : 0,
'config' => $config,
]);
$id = (int) $this->pdo->lastInsertId();
if ($isDefault) {
$this->setDefaultDashboard($ownerKey, $id);
}
return $id;
}
public function updateDashboard(int $id, string $ownerKey, array $data): void
{
if (!$this->available() || $id <= 0 || $ownerKey === '') {
return;
}
$dashboard = $this->getOwnedDashboard($id, $ownerKey);
if ($dashboard === null) {
return;
}
$title = trim((string) ($data['title'] ?? ($dashboard['title'] ?? 'Dashboard')));
$slug = $this->uniqueSlug('nexus_dashboards', $ownerKey, (string) ($data['slug'] ?? $title), $id);
$description = trim((string) ($data['description'] ?? ($dashboard['description'] ?? '')));
$visibility = $this->normalizeVisibility((string) ($data['visibility'] ?? ($dashboard['visibility'] ?? 'private')));
$sortOrder = isset($data['sort_order']) ? (int) $data['sort_order'] : (int) ($dashboard['sort_order'] ?? 0);
$config = $this->encodeJson((array) ($data['config'] ?? ($dashboard['config'] ?? [])));
$isDefault = !empty($data['is_default']);
$stmt = $this->pdo->prepare(
"UPDATE nexus_dashboards
SET title = :title,
slug = :slug,
description = :description,
visibility = :visibility,
sort_order = :sort_order,
is_default = :is_default,
config = :config,
updated_at = CURRENT_TIMESTAMP
WHERE id = :id AND owner_key = :owner_key"
);
$stmt->execute([
'id' => $id,
'owner_key' => $ownerKey,
'title' => $title !== '' ? $title : 'Dashboard',
'slug' => $slug,
'description' => $description,
'visibility' => $visibility,
'sort_order' => $sortOrder,
'is_default' => $isDefault ? 1 : 0,
'config' => $config,
]);
if ($isDefault) {
$this->setDefaultDashboard($ownerKey, $id);
}
}
public function deleteDashboard(int $id, string $ownerKey): void
{
if (!$this->available() || $id <= 0 || $ownerKey === '') {
return;
}
$dashboard = $this->getOwnedDashboard($id, $ownerKey);
if ($dashboard === null) {
return;
}
$stmt = $this->pdo->prepare("DELETE FROM nexus_dashboard_items WHERE dashboard_id = :dashboard_id");
$stmt->execute(['dashboard_id' => $id]);
$stmt = $this->pdo->prepare("DELETE FROM nexus_dashboard_shares WHERE dashboard_id = :dashboard_id");
$stmt->execute(['dashboard_id' => $id]);
$stmt = $this->pdo->prepare("DELETE FROM nexus_dashboards WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
if (!empty($dashboard['is_default'])) {
$remaining = $this->listDashboardsForOwner($ownerKey);
if ($remaining !== []) {
$this->setDefaultDashboard($ownerKey, (int) $remaining[0]['id']);
}
}
}
public function setDefaultDashboard(string $ownerKey, int $dashboardId): void
{
if (!$this->available() || $ownerKey === '' || $dashboardId <= 0) {
return;
}
$stmt = $this->pdo->prepare("UPDATE nexus_dashboards SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE owner_key = :owner_key");
$stmt->execute(['owner_key' => $ownerKey]);
$stmt = $this->pdo->prepare("UPDATE nexus_dashboards SET is_default = 1, updated_at = CURRENT_TIMESTAMP WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $dashboardId, 'owner_key' => $ownerKey]);
}
public function listItems(int $dashboardId): array
{
if (!$this->available() || $dashboardId <= 0) {
return [];
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_dashboard_items
WHERE dashboard_id = :dashboard_id
ORDER BY sort_order ASC, id ASC"
);
$stmt->execute(['dashboard_id' => $dashboardId]);
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
}
public function createItem(int $dashboardId, string $ownerKey, array $data): int
{
if (!$this->available() || $dashboardId <= 0) {
return 0;
}
$dashboard = $this->getOwnedDashboard($dashboardId, $ownerKey);
if ($dashboard === null) {
return 0;
}
$itemType = $this->normalizeItemType((string) ($data['item_type'] ?? 'link'));
$title = trim((string) ($data['title'] ?? 'Element'));
$description = trim((string) ($data['description'] ?? ''));
$gridColumn = $this->nullableInt($data['grid_column'] ?? null);
$gridRow = $this->nullableInt($data['grid_row'] ?? null);
$columnSpan = max(1, min(4, (int) ($data['column_span'] ?? 1)));
$rowSpan = max(1, min(4, (int) ($data['row_span'] ?? 1)));
$sortOrder = (int) ($data['sort_order'] ?? $this->nextItemSortOrder($dashboardId));
$config = $this->encodeJson((array) ($data['config'] ?? []));
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_dashboard_items
(dashboard_id, item_type, title, description, grid_column, grid_row, column_span, row_span, sort_order, config, created_at, updated_at)
VALUES
(:dashboard_id, :item_type, :title, :description, :grid_column, :grid_row, :column_span, :row_span, :sort_order, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'dashboard_id' => $dashboardId,
'item_type' => $itemType,
'title' => $title !== '' ? $title : 'Element',
'description' => $description,
'grid_column' => $gridColumn,
'grid_row' => $gridRow,
'column_span' => $columnSpan,
'row_span' => $rowSpan,
'sort_order' => $sortOrder,
'config' => $config,
]);
return (int) $this->pdo->lastInsertId();
}
public function deleteItem(int $itemId, int $dashboardId, string $ownerKey): void
{
if (!$this->available() || $itemId <= 0 || $dashboardId <= 0) {
return;
}
$dashboard = $this->getOwnedDashboard($dashboardId, $ownerKey);
if ($dashboard === null) {
return;
}
$stmt = $this->pdo->prepare("DELETE FROM nexus_dashboard_items WHERE id = :id AND dashboard_id = :dashboard_id");
$stmt->execute([
'id' => $itemId,
'dashboard_id' => $dashboardId,
]);
}
public function listIntegrationsForOwner(string $ownerKey): array
{
if (!$this->available() || $ownerKey === '') {
return [];
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_integrations
WHERE owner_key = :owner_key
ORDER BY is_active DESC, name ASC, id ASC"
);
$stmt->execute(['owner_key' => $ownerKey]);
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
}
public function createIntegration(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
return 0;
}
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_integrations
(owner_key, type, name, base_url, visibility, is_active, config, secrets, created_at, updated_at)
VALUES
(:owner_key, :type, :name, :base_url, :visibility, :is_active, :config, :secrets, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'owner_key' => $ownerKey,
'type' => trim((string) ($data['type'] ?? 'generic')) ?: 'generic',
'name' => trim((string) ($data['name'] ?? 'Integration')) ?: 'Integration',
'base_url' => trim((string) ($data['base_url'] ?? '')),
'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')),
'is_active' => !empty($data['is_active']) ? 1 : 0,
'config' => $this->encodeJson((array) ($data['config'] ?? [])),
'secrets' => $this->encodeJson((array) ($data['secrets'] ?? [])),
]);
return (int) $this->pdo->lastInsertId();
}
public function deleteIntegration(int $id, string $ownerKey): void
{
if (!$this->available() || $id <= 0 || $ownerKey === '') {
return;
}
$stmt = $this->pdo->prepare("DELETE FROM nexus_integrations WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
}
public function listPageModulesForOwner(string $ownerKey): array
{
if (!$this->available() || $ownerKey === '') {
return [];
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_page_modules
WHERE owner_key = :owner_key
ORDER BY is_active DESC, title ASC, id ASC"
);
$stmt->execute(['owner_key' => $ownerKey]);
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
}
public function createPageModule(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
return 0;
}
$title = trim((string) ($data['title'] ?? 'Seitenmodul')) ?: 'Seitenmodul';
$slug = $this->uniqueSlug('nexus_page_modules', $ownerKey, (string) ($data['slug'] ?? $title), 0);
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_page_modules
(owner_key, title, slug, module_type, target_url, description, visibility, open_mode, is_active, config, created_at, updated_at)
VALUES
(:owner_key, :title, :slug, :module_type, :target_url, :description, :visibility, :open_mode, :is_active, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'owner_key' => $ownerKey,
'title' => $title,
'slug' => $slug,
'module_type' => $this->normalizePageModuleType((string) ($data['module_type'] ?? 'link')),
'target_url' => trim((string) ($data['target_url'] ?? '')),
'description' => trim((string) ($data['description'] ?? '')),
'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')),
'open_mode' => $this->normalizeOpenMode((string) ($data['open_mode'] ?? 'embed')),
'is_active' => !empty($data['is_active']) ? 1 : 0,
'config' => $this->encodeJson((array) ($data['config'] ?? [])),
]);
return (int) $this->pdo->lastInsertId();
}
public function deletePageModule(int $id, string $ownerKey): void
{
if (!$this->available() || $id <= 0 || $ownerKey === '') {
return;
}
$stmt = $this->pdo->prepare("DELETE FROM nexus_page_modules WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
}
public function getPageModule(int $id, string $ownerKey = '', array $groups = []): ?array
{
if (!$this->available() || $id <= 0) {
return null;
}
$stmt = $this->pdo->prepare("SELECT * FROM nexus_page_modules WHERE id = :id LIMIT 1");
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!is_array($row)) {
return null;
}
$module = $this->hydrateRow($row);
if ($module['owner_key'] === $ownerKey || $module['visibility'] === 'public') {
return $module;
}
if ($module['visibility'] === 'group' && $ownerKey !== '') {
foreach ($groups as $group) {
if (in_array($group, (array) ($module['config']['groups'] ?? []), true)) {
return $module;
}
}
}
return null;
}
private function listSharedDashboardIds(string $ownerKey, array $groups): array
{
$result = [];
$stmt = $this->pdo->prepare(
"SELECT dashboard_id, share_type, share_value
FROM nexus_dashboard_shares
WHERE share_type = 'user' AND share_value = :share_value"
);
$stmt->execute(['share_value' => $ownerKey]);
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) {
$result[] = (int) ($row['dashboard_id'] ?? 0);
}
foreach ($groups as $group) {
$group = trim((string) $group);
if ($group === '') {
continue;
}
$stmt = $this->pdo->prepare(
"SELECT dashboard_id
FROM nexus_dashboard_shares
WHERE share_type = 'group' AND share_value = :share_value"
);
$stmt->execute(['share_value' => $group]);
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [] as $row) {
$result[] = (int) ($row['dashboard_id'] ?? 0);
}
}
return array_values(array_unique(array_filter($result, static fn (int $id): bool => $id > 0)));
}
private function getOwnedDashboard(int $id, string $ownerKey): ?array
{
$dashboard = $this->getDashboardById($id);
if ($dashboard === null || (string) ($dashboard['owner_key'] ?? '') !== $ownerKey) {
return null;
}
return $dashboard;
}
private function uniqueSlug(string $table, string $ownerKey, string $candidate, int $ignoreId): string
{
$slug = $this->slugify($candidate);
$suffix = 1;
while ($this->slugExists($table, $ownerKey, $slug, $ignoreId)) {
$suffix++;
$slug = $this->slugify($candidate) . '-' . $suffix;
}
return $slug;
}
private function slugExists(string $table, string $ownerKey, string $slug, int $ignoreId): bool
{
$sql = "SELECT id FROM {$table} WHERE owner_key = :owner_key AND slug = :slug";
if ($ignoreId > 0) {
$sql .= " AND id <> :id";
}
$sql .= " LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$params = ['owner_key' => $ownerKey, 'slug' => $slug];
if ($ignoreId > 0) {
$params['id'] = $ignoreId;
}
$stmt->execute($params);
return (bool) $stmt->fetchColumn();
}
private function slugify(string $value): string
{
$value = trim(mb_strtolower($value));
$value = preg_replace('/[^a-z0-9]+/u', '-', $value) ?? '';
$value = trim($value, '-');
return $value !== '' ? $value : 'eintrag';
}
private function nextSortOrder(string $table, string $field, string $value): int
{
$stmt = $this->pdo->prepare("SELECT COALESCE(MAX(sort_order), 0) + 10 FROM {$table} WHERE {$field} = :value");
$stmt->execute(['value' => $value]);
return (int) $stmt->fetchColumn();
}
private function nextItemSortOrder(int $dashboardId): int
{
$stmt = $this->pdo->prepare("SELECT COALESCE(MAX(sort_order), 0) + 10 FROM nexus_dashboard_items WHERE dashboard_id = :dashboard_id");
$stmt->execute(['dashboard_id' => $dashboardId]);
return (int) $stmt->fetchColumn();
}
private function normalizeVisibility(string $value): string
{
$value = trim($value);
return in_array($value, ['private', 'group', 'public'], true) ? $value : 'private';
}
private function normalizeItemType(string $value): string
{
$value = trim($value);
return in_array($value, ['link', 'iframe', 'page_module', 'bookmark_group', 'module_link'], true) ? $value : 'link';
}
private function normalizePageModuleType(string $value): string
{
$value = trim($value);
return in_array($value, ['link', 'iframe', 'bookmark_group', 'external_status'], true) ? $value : 'link';
}
private function normalizeOpenMode(string $value): string
{
$value = trim($value);
return in_array($value, ['embed', 'new_tab', 'same_tab'], true) ? $value : 'embed';
}
private function encodeJson(array $value): string
{
$encoded = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return $encoded === false ? '{}' : $encoded;
}
private function decodeJson(mixed $value): array
{
if (!is_string($value) || trim($value) === '') {
return [];
}
$decoded = json_decode($value, true);
return is_array($decoded) ? $decoded : [];
}
private function hydrateRows(array $rows): array
{
return array_values(array_map(fn (array $row): array => $this->hydrateRow($row), $rows));
}
private function hydrateRow(array $row): array
{
foreach (['config', 'secrets'] as $jsonField) {
if (array_key_exists($jsonField, $row)) {
$row[$jsonField] = $this->decodeJson($row[$jsonField]);
}
}
foreach (['is_default', 'is_active'] as $boolField) {
if (array_key_exists($boolField, $row)) {
$row[$boolField] = !empty($row[$boolField]);
}
}
return $row;
}
private function nullableInt(mixed $value): ?int
{
if ($value === null || $value === '') {
return null;
}
return (int) $value;
}
}

View File

@@ -90,6 +90,23 @@ function auth_display_name(): string
return $email;
}
function auth_user_key(): string
{
$user = auth_user();
if (!$user) {
return '';
}
foreach (['sub', 'email', 'username', 'name'] as $field) {
$value = trim((string) ($user[$field] ?? ''));
if ($value !== '') {
return $value;
}
}
return '';
}
function auth_initials(): string
{
$name = auth_display_name();
@@ -188,6 +205,11 @@ function modules(): \App\ModuleManager
return app()->modules();
}
function dashboards(): \App\NexusDashboardService
{
return app()->dashboards();
}
function module_fn(string $module, string $name, mixed ...$args): mixed
{
return modules()->call($module, $name, ...$args);