Initial commit: enterprise digital platform with portal SSO, DigiServer, IT Assets, NetworkView, Server Monitor

This commit is contained in:
ske087
2026-05-10 21:07:50 +03:00
commit 8d9df56b0b
364 changed files with 73655 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
{% extends 'base.html' %}
{% block title %}Sign In — Enterprise Digital Platform{% endblock %}
{% block content %}
<div class="login-wrapper">
<div class="login-card">
<div class="login-logo">
<span class="brand-icon-lg"></span>
<h1 class="login-title">Enterprise Digital Platform</h1>
<p class="login-subtitle">Sign in to continue</p>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash flash-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('auth.login', next=request.args.get('next', '')) }}" autocomplete="off">
<div class="form-group">
<label for="username" class="form-label">Username</label>
<input
id="username"
name="username"
type="text"
class="form-input"
placeholder="Enter username"
required
autofocus
/>
</div>
<div class="form-group">
<label for="password" class="form-label">Password</label>
<input
id="password"
name="password"
type="password"
class="form-input"
placeholder="Enter password"
required
/>
</div>
<button type="submit" class="btn btn-primary btn-full">Sign In</button>
</form>
</div>
</div>
{% endblock %}
+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Enterprise Digital Platform{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/portal.css') }}" />
</head>
<body>
{% if current_user.is_authenticated %}
<header class="topbar">
<div class="topbar-brand">
<span class="brand-icon"></span>
<span class="brand-name">Enterprise Digital Platform</span>
</div>
<nav class="topbar-nav">
<a href="{{ url_for('dashboard.index') }}" class="nav-link {% if request.endpoint == 'dashboard.index' %}active{% endif %}">Dashboard</a>
{% if current_user.is_admin %}
<a href="{{ url_for('settings.index') }}" class="nav-link {% if request.blueprint == 'settings' %}active{% endif %}">Settings</a>
{% endif %}
</nav>
<div class="topbar-user">
<span class="user-badge">{{ current_user.username[0].upper() }}</span>
<span class="user-name">{{ current_user.username }}</span>
{% if current_user.is_admin %}<span class="role-tag">admin</span>{% endif %}
<a href="{{ url_for('auth.logout') }}" class="btn-logout" title="Sign out"></a>
</div>
</header>
{% endif %}
<main class="page-content">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-container">
{% for category, message in messages %}
<div class="flash flash-{{ category }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
</html>
@@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block title %}Access Denied — Enterprise Digital Platform{% endblock %}
{% block content %}
<div class="error-wrapper">
<div class="error-card">
<div class="error-icon">🚫</div>
<h2>Access Denied</h2>
<p>You do not have permission to access this application.<br>Contact your administrator to request access.</p>
<a href="{{ url_for('dashboard.index') }}" class="btn btn-primary">← Back to Dashboard</a>
</div>
</div>
{% endblock %}
+45
View File
@@ -0,0 +1,45 @@
{% extends 'base.html' %}
{% block title %}Dashboard — Enterprise Digital Platform{% endblock %}
{% block content %}
<div class="dashboard-wrapper">
<div class="dashboard-header">
<h2 class="section-title">Applications</h2>
<p class="section-subtitle">Click <strong>Open</strong> to launch an application. Your session carries over automatically.</p>
</div>
<div class="app-grid">
{% for app in apps %}
<div class="app-card {% if not app.has_access %}app-card--locked{% endif %}" style="--accent: {{ app.color }};">
<div class="app-card-header">
<span class="app-icon">{{ app.icon }}</span>
<div class="app-status">
{% if app.has_access %}
<span class="status-dot status-dot--active"></span>
<span class="status-label">Access granted</span>
{% else %}
<span class="status-dot status-dot--inactive"></span>
<span class="status-label">No access</span>
{% endif %}
</div>
</div>
<div class="app-card-body">
<h3 class="app-name">{{ app.name }}</h3>
<p class="app-desc">{{ app.description }}</p>
</div>
<div class="app-card-footer">
{% if app.has_access %}
<a href="{{ app.url }}" class="btn btn-app" style="--accent: {{ app.color }};" target="_self">
Open →
</a>
{% else %}
<span class="btn btn-app btn-app--disabled" title="Contact your administrator to request access.">
No Access
</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
+105
View File
@@ -0,0 +1,105 @@
{% extends 'base.html' %}
{% block title %}API Keys — Settings{% endblock %}
{% block content %}
<div class="settings-wrapper">
<div class="settings-header">
<h2 class="section-title">Settings</h2>
<div class="settings-tabs">
<a href="{{ url_for('settings.index') }}" class="tab-link">Users &amp; Access</a>
<a href="{{ url_for('settings.api_keys') }}" class="tab-link active">API Keys</a>
<a href="{{ url_for('settings.modules') }}" class="tab-link">Modules</a>
</div>
</div>
<div class="settings-section">
<div class="section-toolbar">
<h3>API Keys</h3>
</div>
<p class="section-hint">
API keys allow programmatic access to sub-applications.
Pass the key as an <code>X-Api-Key</code> header in your requests.
</p>
<!-- New key form -->
<form method="POST" action="{{ url_for('settings.new_api_key') }}" class="form-inline-card">
<div class="form-row">
<div class="form-group">
<label class="form-label">User</label>
<select name="user_id" class="form-select" required>
<option value="">— select user —</option>
{% for u in users %}
<option value="{{ u.id }}">{{ u.username }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label">Application</label>
<select name="app_name" class="form-select" required>
<option value="">— select app —</option>
{% for app in registered_apps %}
<option value="{{ app['id'] }}">{{ app.icon }} {{ app.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group form-group--grow">
<label class="form-label">Description (optional)</label>
<input type="text" name="description" class="form-input" placeholder="e.g. CI/CD integration" />
</div>
<div class="form-group form-group--action">
<label class="form-label">&nbsp;</label>
<button type="submit" class="btn btn-primary">Generate Key</button>
</div>
</div>
</form>
<!-- Keys table -->
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>User</th>
<th>Application</th>
<th>Key</th>
<th>Description</th>
<th>Status</th>
<th>Created</th>
<th>Last Used</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for k in keys %}
<tr class="{{ 'row-inactive' if not k.is_active }}">
<td>{{ k.user.username }}</td>
<td>{{ k.app_name }}</td>
<td><code class="key-cell">{{ k.key[:16] }}…</code></td>
<td class="text-muted">{{ k.description or '—' }}</td>
<td>
{% if k.is_active %}
<span class="badge badge-active">active</span>
{% else %}
<span class="badge badge-inactive">revoked</span>
{% endif %}
</td>
<td class="text-muted">{{ k.created_at.strftime('%Y-%m-%d') }}</td>
<td class="text-muted">{{ k.last_used_at.strftime('%Y-%m-%d') if k.last_used_at else '—' }}</td>
<td>
{% if k.is_active %}
<form method="POST" action="{{ url_for('settings.revoke_api_key', key_id=k.id) }}"
onsubmit="return confirm('Revoke this API key?')">
<button type="submit" class="btn btn-sm btn-danger">Revoke</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
{% if not keys %}
<tr><td colspan="8" class="empty-row">No API keys yet.</td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
+124
View File
@@ -0,0 +1,124 @@
{% extends 'base.html' %}
{% block title %}Settings — Enterprise Digital Platform{% endblock %}
{% block content %}
<div class="settings-wrapper">
<div class="settings-header">
<h2 class="section-title">Settings</h2>
<div class="settings-tabs">
<a href="{{ url_for('settings.index') }}" class="tab-link active">Users &amp; Access</a>
<a href="{{ url_for('settings.api_keys') }}" class="tab-link">API Keys</a>
<a href="{{ url_for('settings.modules') }}" class="tab-link">Modules</a>
</div>
</div>
<div class="settings-section">
<div class="section-toolbar">
<h3>Portal Users</h3>
<a href="{{ url_for('settings.new_user') }}" class="btn btn-primary btn-sm">+ New User</a>
</div>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Last Login</th>
{% for app in registered_apps %}
<th class="app-col" style="border-top: 3px solid {{ app.color }};">
{{ app.icon }} {{ app.name }}<br/>
<small style="font-weight:400; color:var(--text-muted);">access / role</small>
</th>
{% endfor %}
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td><strong>{{ user.username }}</strong></td>
<td class="text-muted">{{ user.email }}</td>
<td>
{% if user.is_admin %}
<span class="badge badge-admin">admin</span>
{% else %}
<span class="badge badge-user">user</span>
{% endif %}
</td>
<td class="text-muted">
{{ user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else '—' }}
</td>
{# Per-app role dropdown — auto-submits on change #}
{% for app in registered_apps %}
{% set cur_access = user.app_accesses.filter_by(app_name=app['id']).first() %}
{% set cur_role = cur_access.app_role if cur_access and cur_access.is_active and cur_access.app_role else ('active' if (cur_access and cur_access.is_active) else 'none') %}
<td class="center">
<form method="POST" action="{{ url_for('settings.update_access', user_id=user.id) }}"
class="inline-form">
{# Carry all other apps' current values as hidden inputs #}
{% for other in registered_apps %}
{% if other['id'] != app['id'] %}
{% set oa = user.app_accesses.filter_by(app_name=other['id']).first() %}
{% if oa and oa.is_active %}
<input type="hidden" name="role_{{ other['id'] }}"
value="{{ oa.app_role if oa.app_role else 'user' }}" />
{% else %}
<input type="hidden" name="role_{{ other['id'] }}" value="none" />
{% endif %}
{% endif %}
{% endfor %}
<select name="role_{{ app['id'] }}"
class="app-role-select"
style="--app-color: {{ app.color }};"
onchange="this.closest('form').submit()"
title="{{ app['name'] }} access for {{ user.username }}"
{% if user.id == current_user.id and app['id'] == 'portal' %}disabled{% endif %}>
<option value="none" {% if not (cur_access and cur_access.is_active) %}selected{% endif %}>
— no access
</option>
<option value="user"
{% if cur_access and cur_access.is_active and (not cur_access.app_role or cur_access.app_role == 'user') %}selected{% endif %}>
✓ user
</option>
<option value="admin"
{% if cur_access and cur_access.is_active and cur_access.app_role == 'admin' %}selected{% endif %}>
★ admin
</option>
</select>
</form>
</td>
{% endfor %}
<td>
{% if user.id != current_user.id %}
<form method="POST" action="{{ url_for('settings.delete_user', user_id=user.id) }}"
onsubmit="return confirm('Delete user {{ user.username }}?')">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
document.querySelectorAll('.app-role-select').forEach(function(sel) {
function refresh() {
if (sel.value !== 'none') sel.classList.add('has-access');
else sel.classList.remove('has-access');
}
refresh();
sel.addEventListener('change', refresh);
});
</script>
{% endblock %}
+109
View File
@@ -0,0 +1,109 @@
{% extends 'base.html' %}
{% block title %}Modules — Enterprise Digital Platform{% endblock %}
{% block content %}
<div class="settings-wrapper">
<div class="settings-header">
<h2 class="section-title">Settings</h2>
<div class="settings-tabs">
<a href="{{ url_for('settings.index') }}" class="tab-link">Users &amp; Access</a>
<a href="{{ url_for('settings.api_keys') }}" class="tab-link">API Keys</a>
<a href="{{ url_for('settings.modules') }}" class="tab-link active">Modules</a>
</div>
</div>
<div class="settings-section">
<div class="section-toolbar">
<h3>Installed Modules</h3>
<span class="text-muted" style="font-size:0.875rem;">
Enable or disable services to control which applications run on this instance.
</span>
</div>
<div class="app-grid" style="margin-top:1.25rem;">
{% for ms in module_statuses %}
{% set app = ms.app %}
{% set on = ms.enabled %}
{% set alive = ms.running %}
<div class="app-card {% if not on %}app-card--locked{% endif %}"
style="--accent: {{ app.color }};">
<div class="app-card-header">
<span class="app-icon">{{ app.icon }}</span>
<span class="app-status">
<span class="status-dot {% if alive %}status-dot--active{% else %}status-dot--inactive{% endif %}"></span>
<span class="status-label">{% if alive %}running{% else %}stopped{% endif %}</span>
</span>
</div>
<div class="app-card-body">
<div class="app-name">{{ app.name }}</div>
<div class="app-desc" style="margin-top:0.4rem;">{{ app.description }}</div>
</div>
<div class="app-card-footer" style="display:flex; align-items:center; justify-content:space-between; gap:1rem;">
{# Toggle switch — submits a hidden form on change #}
<label class="module-toggle" title="{% if on %}Disable {{ app.name }}{% else %}Enable {{ app.name }}{% endif %}">
<input
type="checkbox"
class="module-toggle-input"
data-app="{{ app.id }}"
data-action-enable="{{ url_for('settings.toggle_module', app_id=app.id) }}"
{% if on %}checked{% endif %}
/>
<span class="toggle-slider"></span>
<span class="toggle-label">{% if on %}Enabled{% else %}Disabled{% endif %}</span>
</label>
{# Hidden forms — one per action #}
<form id="form-enable-{{ app.id }}" method="POST"
action="{{ url_for('settings.toggle_module', app_id=app.id) }}" style="display:none;">
<input type="hidden" name="action" value="enable" />
</form>
<form id="form-disable-{{ app.id }}" method="POST"
action="{{ url_for('settings.toggle_module', app_id=app.id) }}" style="display:none;">
<input type="hidden" name="action" value="disable" />
</form>
{% if on and alive %}
<a href="{{ app.url }}" class="btn btn-sm btn-secondary" target="_blank">Open ↗</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<div class="form-inline-card" style="margin-top:1.5rem; display:flex; align-items:flex-start; gap:0.75rem;">
<span style="font-size:1.25rem; flex-shrink:0;"></span>
<div style="font-size:0.875rem; color:var(--text-secondary); line-height:1.6;">
<strong style="color:var(--text-primary);">How modules work</strong><br/>
Disabling a module sends a stop signal to its process and prevents it from being proxied.
Enabling a module starts its process in the background.
Module state is preserved across page reloads and respected by
<code>./start-dev.sh</code> on next launch.
</div>
</div>
</div>
</div>
<script>
document.querySelectorAll('.module-toggle-input').forEach(function(cb) {
cb.addEventListener('change', function() {
var appId = this.dataset.app;
var action = this.checked ? 'enable' : 'disable';
var confirmed = true;
if (action === 'disable') {
confirmed = confirm(
'Disable and stop this service?\n\n' +
'Users will lose access until it is re-enabled.'
);
}
if (confirmed) {
document.getElementById('form-' + action + '-' + appId).submit();
} else {
// Revert visual state without submitting
this.checked = !this.checked;
}
});
});
</script>
{% endblock %}
@@ -0,0 +1,96 @@
{% extends 'base.html' %}
{% block title %}New User — Settings{% endblock %}
{% block content %}
<div class="settings-wrapper">
<div class="settings-header">
<h2 class="section-title">Settings</h2>
<div class="settings-tabs">
<a href="{{ url_for('settings.index') }}" class="tab-link active">Users &amp; Access</a>
<a href="{{ url_for('settings.api_keys') }}" class="tab-link">API Keys</a>
<a href="{{ url_for('settings.modules') }}" class="tab-link">Modules</a>
</div>
</div>
<div class="settings-section">
<div class="section-toolbar">
<h3>Create New User</h3>
<a href="{{ url_for('settings.index') }}" class="btn btn-sm btn-secondary">← Back</a>
</div>
<form method="POST" class="form-card">
<div class="form-row">
<div class="form-group">
<label class="form-label">Username</label>
<input type="text" name="username" class="form-input" required autofocus />
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-input" required />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-input" required />
</div>
<div class="form-group form-group--check">
<label class="form-label">Portal Role</label>
<label class="toggle">
<input type="checkbox" name="is_admin" />
<span class="toggle-slider"></span>
<span class="toggle-label">Platform administrator</span>
</label>
</div>
</div>
<div class="form-group">
<label class="form-label">Application Access &amp; Role</label>
<p style="font-size:0.82rem; color:var(--text-secondary); margin-bottom:0.75rem;">
Select the access level for each application. "Admin" grants full management rights
inside that app regardless of the portal role above.
</p>
<div class="app-role-grid">
{% for app in registered_apps %}
<div class="app-role-card" style="--app-color: {{ app.color }}; border-top-color: {{ app.color }};">
<div class="app-role-card-header">
<span style="font-size:1.5rem;">{{ app.icon }}</span>
<strong>{{ app.name }}</strong>
</div>
<p class="app-role-desc">{{ app.description }}</p>
<select name="role_{{ app['id'] }}" class="form-select app-role-new-select"
data-color="{{ app.color }}">
<option value="none">— No access</option>
<option value="user">✓ User</option>
<option value="admin">★ Admin</option>
</select>
</div>
{% endfor %}
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create User</button>
<a href="{{ url_for('settings.index') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
<script>
document.querySelectorAll('.app-role-new-select').forEach(function(sel) {
function refresh() {
var color = sel.dataset.color;
if (sel.value !== 'none') {
sel.style.borderColor = color;
sel.style.color = color;
} else {
sel.style.borderColor = '';
sel.style.color = '';
}
}
refresh();
sel.addEventListener('change', refresh);
});
</script>
{% endblock %}