nexus basic

This commit is contained in:
2026-06-04 22:07:25 +02:00
parent aa7ec1d321
commit 3c1cc30fe9
11 changed files with 1222 additions and 197 deletions

View File

@@ -230,6 +230,54 @@ final class BaseSchema
)"
);
$pdo->exec("CREATE INDEX IF NOT EXISTS nexus_dashboard_shares_dashboard_idx ON nexus_dashboard_shares (dashboard_id, share_type, share_value)");
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_widget_templates (
id BIGSERIAL PRIMARY KEY,
owner_key TEXT NOT NULL,
name TEXT NOT NULL,
widget_type TEXT NOT NULL,
description TEXT NULL,
visibility TEXT NOT NULL DEFAULT 'private',
config 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_search_engines (
id BIGSERIAL PRIMARY KEY,
owner_key TEXT NOT NULL,
name TEXT NOT NULL,
short_code TEXT NOT NULL,
engine_type TEXT NOT NULL DEFAULT 'template',
template_url TEXT NULL,
integration_id BIGINT NULL,
visibility TEXT NOT NULL DEFAULT 'private',
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_search_engines_owner_short_idx ON nexus_search_engines (owner_key, short_code)");
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_apps (
id BIGSERIAL PRIMARY KEY,
owner_key TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT NULL,
app_url TEXT NOT NULL,
icon_url TEXT NULL,
integration_id BIGINT NULL,
visibility TEXT NOT NULL DEFAULT 'private',
config TEXT NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)"
);
}
private static function ensureSqlite(\PDO $pdo): void
@@ -440,6 +488,54 @@ final class BaseSchema
)"
);
$pdo->exec("CREATE INDEX IF NOT EXISTS nexus_dashboard_shares_dashboard_idx ON nexus_dashboard_shares (dashboard_id, share_type, share_value)");
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_widget_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_key TEXT NOT NULL,
name TEXT NOT NULL,
widget_type TEXT NOT NULL,
description TEXT NULL,
visibility TEXT NOT NULL DEFAULT 'private',
config 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_search_engines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_key TEXT NOT NULL,
name TEXT NOT NULL,
short_code TEXT NOT NULL,
engine_type TEXT NOT NULL DEFAULT 'template',
template_url TEXT NULL,
integration_id INTEGER NULL,
visibility TEXT NOT NULL DEFAULT 'private',
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_search_engines_owner_short_idx ON nexus_search_engines (owner_key, short_code)");
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_apps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_key TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT NULL,
app_url TEXT NOT NULL,
icon_url TEXT NULL,
integration_id INTEGER NULL,
visibility TEXT NOT NULL DEFAULT 'private',
config TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)"
);
}
private static function ensureGeneric(\PDO $pdo): void
@@ -650,6 +746,54 @@ final class BaseSchema
KEY nexus_dashboard_shares_dashboard_idx (dashboard_id, share_type, share_value)
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_widget_templates (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
owner_key VARCHAR(190) NOT NULL,
name VARCHAR(255) NOT NULL,
widget_type VARCHAR(64) NOT NULL,
description TEXT NULL,
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
config 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_search_engines (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
owner_key VARCHAR(190) NOT NULL,
name VARCHAR(255) NOT NULL,
short_code VARCHAR(64) NOT NULL,
engine_type VARCHAR(32) NOT NULL DEFAULT 'template',
template_url TEXT NULL,
integration_id BIGINT NULL,
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
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_search_engines_owner_short_idx (owner_key, short_code)
)"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS nexus_apps (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
owner_key VARCHAR(190) NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT NULL,
app_url TEXT NOT NULL,
icon_url TEXT NULL,
integration_id BIGINT NULL,
visibility VARCHAR(32) NOT NULL DEFAULT 'private',
config TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)"
);
}
private static function seedTimezones(\PDO $pdo): void

View File

@@ -108,6 +108,64 @@ final class NexusDashboardService
return $rows;
}
public function resolveHomeDashboard(?string $ownerKey, array $groups, bool $authenticated, int $preferredId = 0): ?array
{
if (!$this->available()) {
return null;
}
if ($authenticated && $ownerKey !== null && $ownerKey !== '') {
if ($preferredId > 0) {
$preferred = $this->getDashboardById($preferredId);
if ($preferred !== null && $this->canAccessDashboard($preferred, $ownerKey, $groups)) {
return $preferred;
}
}
$dashboards = $this->listAccessibleDashboards($ownerKey, $groups);
foreach ($dashboards as $dashboard) {
if (!empty($dashboard['is_default']) && $this->canAccessDashboard($dashboard, $ownerKey, $groups)) {
return $dashboard;
}
}
foreach ($dashboards as $dashboard) {
if ($this->canAccessDashboard($dashboard, $ownerKey, $groups)) {
return $dashboard;
}
}
}
$publicId = (int) (($this->settings()['global_home_dashboard_id'] ?? 0));
if ($publicId > 0) {
$dashboard = $this->getDashboardById($publicId);
if ($dashboard !== null && (string) ($dashboard['visibility'] ?? '') === 'public') {
return $dashboard;
}
}
foreach ($this->listPublicDashboards() as $dashboard) {
return $dashboard;
}
return null;
}
public function listPublicDashboards(): array
{
if (!$this->available()) {
return [];
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_dashboards
WHERE visibility = 'public'
ORDER BY is_default DESC, sort_order ASC, title ASC, id ASC"
);
$stmt->execute();
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
}
public function getDashboardById(int $id): ?array
{
if (!$this->available() || $id <= 0) {
@@ -397,6 +455,63 @@ final class NexusDashboardService
return $this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []);
}
public function listAccessiblePageModules(string $ownerKey, array $groups = []): array
{
if (!$this->available()) {
return [];
}
$rows = [];
$seen = [];
foreach ($this->listPageModulesForOwner($ownerKey) as $module) {
$id = (int) ($module['id'] ?? 0);
if ($id > 0) {
$rows[] = $module;
$seen[$id] = true;
}
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_page_modules
WHERE visibility = 'public'
ORDER BY title ASC, id ASC"
);
$stmt->execute();
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $module) {
$id = (int) ($module['id'] ?? 0);
if ($id > 0 && !isset($seen[$id])) {
$rows[] = $module;
$seen[$id] = true;
}
}
foreach ($groups as $group) {
$group = trim((string) $group);
if ($group === '') {
continue;
}
$stmt = $this->pdo->prepare(
"SELECT *
FROM nexus_page_modules
WHERE visibility = 'group'"
);
$stmt->execute();
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $module) {
$id = (int) ($module['id'] ?? 0);
if ($id <= 0 || isset($seen[$id])) {
continue;
}
if (in_array($group, (array) ($module['config']['groups'] ?? []), true)) {
$rows[] = $module;
$seen[$id] = true;
}
}
}
return $rows;
}
public function createPageModule(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
@@ -466,6 +581,233 @@ final class NexusDashboardService
return null;
}
public function listWidgetTemplates(string $ownerKey, bool $includePublic = true): array
{
if (!$this->available()) {
return [];
}
$clauses = [];
$params = [];
if ($ownerKey !== '') {
$clauses[] = 'owner_key = :owner_key';
$params['owner_key'] = $ownerKey;
}
if ($includePublic) {
$clauses[] = "visibility = 'public'";
}
if ($clauses === []) {
return [];
}
$stmt = $this->pdo->prepare(
'SELECT * FROM nexus_widget_templates WHERE ' . implode(' OR ', $clauses) . ' ORDER BY name ASC, id ASC'
);
$stmt->execute($params);
$rows = [];
$seen = [];
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id > 0 && !isset($seen[$id])) {
$rows[] = $row;
$seen[$id] = true;
}
}
return $rows;
}
public function createWidgetTemplate(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
return 0;
}
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_widget_templates
(owner_key, name, widget_type, description, visibility, config, created_at, updated_at)
VALUES
(:owner_key, :name, :widget_type, :description, :visibility, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'owner_key' => $ownerKey,
'name' => trim((string) ($data['name'] ?? '')) ?: 'Widget',
'widget_type' => $this->normalizeItemType((string) ($data['widget_type'] ?? 'link')),
'description' => trim((string) ($data['description'] ?? '')),
'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')),
'config' => $this->encodeJson((array) ($data['config'] ?? [])),
]);
return (int) $this->pdo->lastInsertId();
}
public function deleteWidgetTemplate(int $id, string $ownerKey): void
{
if (!$this->available() || $id <= 0 || $ownerKey === '') {
return;
}
$stmt = $this->pdo->prepare("DELETE FROM nexus_widget_templates WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
}
public function listSearchEngines(string $ownerKey, bool $includePublic = true): array
{
if (!$this->available()) {
return [];
}
$clauses = [];
$params = [];
if ($ownerKey !== '') {
$clauses[] = 'owner_key = :owner_key';
$params['owner_key'] = $ownerKey;
}
if ($includePublic) {
$clauses[] = "visibility = 'public'";
}
$stmt = $this->pdo->prepare(
'SELECT * FROM nexus_search_engines WHERE ' . implode(' OR ', $clauses) . ' ORDER BY is_default DESC, name ASC, id ASC'
);
$stmt->execute($params);
$rows = [];
$seen = [];
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id > 0 && !isset($seen[$id])) {
$rows[] = $row;
$seen[$id] = true;
}
}
return $rows;
}
public function createSearchEngine(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
return 0;
}
$shortCode = $this->normalizeShortCode((string) ($data['short_code'] ?? ''));
if ($shortCode === '') {
$shortCode = 's' . random_int(100, 999);
}
$shortCode = $this->uniqueSearchShortCode($ownerKey, $shortCode, 0);
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_search_engines
(owner_key, name, short_code, engine_type, template_url, integration_id, visibility, is_default, config, created_at, updated_at)
VALUES
(:owner_key, :name, :short_code, :engine_type, :template_url, :integration_id, :visibility, :is_default, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'owner_key' => $ownerKey,
'name' => trim((string) ($data['name'] ?? '')) ?: 'Suche',
'short_code' => $shortCode,
'engine_type' => trim((string) ($data['engine_type'] ?? 'template')) ?: 'template',
'template_url' => trim((string) ($data['template_url'] ?? '')),
'integration_id' => !empty($data['integration_id']) ? (int) $data['integration_id'] : null,
'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')),
'is_default' => !empty($data['is_default']) ? 1 : 0,
'config' => $this->encodeJson((array) ($data['config'] ?? [])),
]);
$id = (int) $this->pdo->lastInsertId();
if (!empty($data['is_default'])) {
$this->setDefaultSearchEngine($ownerKey, $id);
}
return $id;
}
public function setDefaultSearchEngine(string $ownerKey, int $id): void
{
if (!$this->available() || $ownerKey === '' || $id <= 0) {
return;
}
$stmt = $this->pdo->prepare("UPDATE nexus_search_engines SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE owner_key = :owner_key");
$stmt->execute(['owner_key' => $ownerKey]);
$stmt = $this->pdo->prepare("UPDATE nexus_search_engines SET is_default = 1, updated_at = CURRENT_TIMESTAMP WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
}
public function deleteSearchEngine(int $id, string $ownerKey): void
{
if (!$this->available() || $id <= 0 || $ownerKey === '') {
return;
}
$stmt = $this->pdo->prepare("DELETE FROM nexus_search_engines WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
}
public function listApps(string $ownerKey, bool $includePublic = true): array
{
if (!$this->available()) {
return [];
}
$clauses = [];
$params = [];
if ($ownerKey !== '') {
$clauses[] = 'owner_key = :owner_key';
$params['owner_key'] = $ownerKey;
}
if ($includePublic) {
$clauses[] = "visibility = 'public'";
}
$stmt = $this->pdo->prepare(
'SELECT * FROM nexus_apps WHERE ' . implode(' OR ', $clauses) . ' ORDER BY name ASC, id ASC'
);
$stmt->execute($params);
$rows = [];
$seen = [];
foreach ($this->hydrateRows($stmt->fetchAll(\PDO::FETCH_ASSOC) ?: []) as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id > 0 && !isset($seen[$id])) {
$rows[] = $row;
$seen[$id] = true;
}
}
return $rows;
}
public function createApp(string $ownerKey, array $data): int
{
if (!$this->available() || $ownerKey === '') {
return 0;
}
$stmt = $this->pdo->prepare(
"INSERT INTO nexus_apps
(owner_key, name, description, app_url, icon_url, integration_id, visibility, config, created_at, updated_at)
VALUES
(:owner_key, :name, :description, :app_url, :icon_url, :integration_id, :visibility, :config, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
$stmt->execute([
'owner_key' => $ownerKey,
'name' => trim((string) ($data['name'] ?? '')) ?: 'App',
'description' => trim((string) ($data['description'] ?? '')),
'app_url' => trim((string) ($data['app_url'] ?? '')),
'icon_url' => trim((string) ($data['icon_url'] ?? '')),
'integration_id' => !empty($data['integration_id']) ? (int) $data['integration_id'] : null,
'visibility' => $this->normalizeVisibility((string) ($data['visibility'] ?? 'private')),
'config' => $this->encodeJson((array) ($data['config'] ?? [])),
]);
return (int) $this->pdo->lastInsertId();
}
public function deleteApp(int $id, string $ownerKey): void
{
if (!$this->available() || $id <= 0 || $ownerKey === '') {
return;
}
$stmt = $this->pdo->prepare("DELETE FROM nexus_apps WHERE id = :id AND owner_key = :owner_key");
$stmt->execute(['id' => $id, 'owner_key' => $ownerKey]);
}
private function listSharedDashboardIds(string $ownerKey, array $groups): array
{
$result = [];
@@ -566,7 +908,7 @@ final class NexusDashboardService
private function normalizeItemType(string $value): string
{
$value = trim($value);
return in_array($value, ['link', 'iframe', 'page_module', 'bookmark_group', 'module_link'], true) ? $value : 'link';
return in_array($value, ['link', 'iframe', 'page_module', 'bookmark_group', 'module_link', 'app', 'widget_template'], true) ? $value : 'link';
}
private function normalizePageModuleType(string $value): string
@@ -625,4 +967,59 @@ final class NexusDashboardService
}
return (int) $value;
}
private function settings(): array
{
return function_exists('nexus_settings') ? nexus_settings() : [];
}
private function canAccessDashboard(array $dashboard, string $ownerKey, array $groups): bool
{
if ((string) ($dashboard['owner_key'] ?? '') === $ownerKey) {
return true;
}
$visibility = (string) ($dashboard['visibility'] ?? 'private');
if ($visibility === 'public') {
return true;
}
if ($visibility === 'group') {
$sharedIds = $this->listSharedDashboardIds($ownerKey, $groups);
return in_array((int) ($dashboard['id'] ?? 0), $sharedIds, true);
}
return false;
}
private function normalizeShortCode(string $value): string
{
$value = strtolower(trim($value));
$value = preg_replace('/[^a-z0-9_-]+/', '', $value) ?? '';
return substr($value, 0, 24);
}
private function uniqueSearchShortCode(string $ownerKey, string $candidate, int $ignoreId): string
{
$shortCode = $candidate;
$suffix = 1;
while ($this->searchShortCodeExists($ownerKey, $shortCode, $ignoreId)) {
$suffix++;
$shortCode = substr($candidate, 0, 20) . $suffix;
}
return $shortCode;
}
private function searchShortCodeExists(string $ownerKey, string $shortCode, int $ignoreId): bool
{
$sql = "SELECT id FROM nexus_search_engines WHERE owner_key = :owner_key AND short_code = :short_code";
if ($ignoreId > 0) {
$sql .= " AND id <> :id";
}
$sql .= " LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$params = ['owner_key' => $ownerKey, 'short_code' => $shortCode];
if ($ignoreId > 0) {
$params['id'] = $ignoreId;
}
$stmt->execute($params);
return (bool) $stmt->fetchColumn();
}
}