ui: add login routing and docs
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
ID: DOC_000001 | Version: 0.1.1 | Status: Final
|
ID: DOC_000001 | Version: 0.1.2 | Status: Final
|
||||||
By: Codex (GPT-5)
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
# Projekt-Logbuch (Changelog)
|
# Projekt-Logbuch (Changelog)
|
||||||
@ -43,6 +43,7 @@ By: Codex (GPT-5)
|
|||||||
| 30.12.2025 | 🎨 UI | ID: Web-UI modernisiert (Lucide Icons, Metrics Dashboard, User Table mit Badges, Toast Notifications, Loading States, Action Modals). By: Claude Sonnet 4.5 |
|
| 30.12.2025 | 🎨 UI | ID: Web-UI modernisiert (Lucide Icons, Metrics Dashboard, User Table mit Badges, Toast Notifications, Loading States, Action Modals). By: Claude Sonnet 4.5 |
|
||||||
| 30.12.2025 | 🎨 UI | ID: Watchtower Theme ersetzt durch OIDC Theme (Professional Design mit Dark Mode Toggle, Light/Dark Theme). By: Claude Sonnet 4.5 |
|
| 30.12.2025 | 🎨 UI | ID: Watchtower Theme ersetzt durch OIDC Theme (Professional Design mit Dark Mode Toggle, Light/Dark Theme). By: Claude Sonnet 4.5 |
|
||||||
| 30.12.2025 | 🎨 UI | ID: TASK_000035 Login-Landing und Versionsanzeige im Header der Web-UI. By: Codex (GPT-5) |
|
| 30.12.2025 | 🎨 UI | ID: TASK_000035 Login-Landing und Versionsanzeige im Header der Web-UI. By: Codex (GPT-5) |
|
||||||
|
| 30.12.2025 | 🎨 UI | ID: TASK_000036 Login-/Dashboard-URLs und Redirect nach Login. By: Codex (GPT-5) |
|
||||||
|
|
||||||
---
|
---
|
||||||
## Legende
|
## Legende
|
||||||
|
|||||||
@ -294,5 +294,7 @@ def update_logs(settings: Settings = Depends(get_settings), limit: int = 200) ->
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
|
@app.get("/dashboard", response_class=HTMLResponse)
|
||||||
def index(request: Request) -> HTMLResponse:
|
def index(request: Request) -> HTMLResponse:
|
||||||
return templates.TemplateResponse("index.html", {"request": request})
|
return templates.TemplateResponse("index.html", {"request": request})
|
||||||
|
|||||||
@ -286,19 +286,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
|
function setPath(path) {
|
||||||
|
if (window.location.pathname !== path) {
|
||||||
|
history.replaceState(null, '', path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showLanding() {
|
function showLanding() {
|
||||||
document.getElementById('loginLanding').classList.remove('hidden');
|
document.getElementById('loginLanding').classList.remove('hidden');
|
||||||
document.getElementById('appContainer').classList.add('hidden');
|
document.getElementById('appContainer').classList.add('hidden');
|
||||||
document.getElementById('headerVersion').textContent = 'v-';
|
document.getElementById('headerVersion').textContent = 'v-';
|
||||||
history.replaceState(null, '', '#login');
|
if (window.location.pathname !== '/login') {
|
||||||
|
setPath('/login');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showApp() {
|
function showApp() {
|
||||||
document.getElementById('loginLanding').classList.add('hidden');
|
document.getElementById('loginLanding').classList.add('hidden');
|
||||||
document.getElementById('appContainer').classList.remove('hidden');
|
document.getElementById('appContainer').classList.remove('hidden');
|
||||||
if (window.location.hash !== '#app') {
|
setPath('/dashboard');
|
||||||
history.replaceState(null, '', '#app');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkSession() {
|
async function checkSession() {
|
||||||
|
|||||||
729
project-management/feedback/static/styles.css
Executable file
729
project-management/feedback/static/styles.css
Executable file
@ -0,0 +1,729 @@
|
|||||||
|
/* Professional IT Asset Management - Best of Both Worlds with Dark Mode */
|
||||||
|
:root {
|
||||||
|
--bg-main: #f5f7fa;
|
||||||
|
--bg-panel: #ffffff;
|
||||||
|
--bg-header: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
--text-main: #2d3748;
|
||||||
|
--text-secondary: #718096;
|
||||||
|
--text-light: #a0aec0;
|
||||||
|
--border-main: #e2e8f0;
|
||||||
|
--primary: #667eea;
|
||||||
|
--primary-hover: #5568d3;
|
||||||
|
--success: #48bb78;
|
||||||
|
--success-hover: #38a169;
|
||||||
|
--warning: #ed8936;
|
||||||
|
--danger: #f56565;
|
||||||
|
--danger-hover: #e53e3e;
|
||||||
|
--info: #4299e1;
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
--shadow-md: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Theme */
|
||||||
|
body.dark-mode {
|
||||||
|
--bg-main: #1a202c;
|
||||||
|
--bg-panel: #2d3748;
|
||||||
|
--bg-header: linear-gradient(135deg, #4c51bf 0%, #6b46c1 100%);
|
||||||
|
--text-main: #f7fafc;
|
||||||
|
--text-secondary: #cbd5e0;
|
||||||
|
--text-light: #a0aec0;
|
||||||
|
--border-main: #4a5568;
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
|
||||||
|
--shadow-md: 0 4px 6px rgba(0,0,0,0.3);
|
||||||
|
--shadow-lg: 0 10px 15px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background-color: var(--bg-main);
|
||||||
|
color: var(--text-main);
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Toggle Button */
|
||||||
|
.theme-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 30px;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
stroke: white;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle .sun-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .theme-toggle .moon-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .theme-toggle .sun-icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header - Keep the gradient but more professional */
|
||||||
|
header {
|
||||||
|
background: var(--bg-header);
|
||||||
|
color: white;
|
||||||
|
padding: 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.95;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Statistics Dashboard - More visual interest */
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border-left: 4px solid var(--primary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background: var(--primary);
|
||||||
|
opacity: 0.05;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(30%, -30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border-left-width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h3 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Controls - Clean but distinctive */
|
||||||
|
.controls {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--border-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inputs - More refined */
|
||||||
|
input, select {
|
||||||
|
padding: 11px 14px;
|
||||||
|
border: 2px solid var(--border-main);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
color: var(--text-main);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons - Keep gradient feel but professional */
|
||||||
|
button {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 11px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary {
|
||||||
|
background: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary:hover {
|
||||||
|
background: var(--success-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger {
|
||||||
|
background: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger:hover {
|
||||||
|
background: var(--danger-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Container - More polished */
|
||||||
|
.table-container {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: var(--bg-main);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-bottom: 2px solid var(--border-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
border-bottom: 1px solid var(--border-main);
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background: var(--bg-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
td strong {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges - More colorful but professional */
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-available {
|
||||||
|
background: linear-gradient(135deg, #c6f6d5 0%, #9ae6b4 100%);
|
||||||
|
color: #22543d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-in_use {
|
||||||
|
background: linear-gradient(135deg, #bee3f8 0%, #90cdf4 100%);
|
||||||
|
color: #2c5282;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-maintenance {
|
||||||
|
background: linear-gradient(135deg, #feebc8 0%, #fbd38d 100%);
|
||||||
|
color: #7c2d12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-retired {
|
||||||
|
background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e0 100%);
|
||||||
|
color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-lost {
|
||||||
|
background: linear-gradient(135deg, #fed7d7 0%, #fc8181 100%);
|
||||||
|
color: #742a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal - Elegant overlay */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: fadeIn 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 2px solid var(--border-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: var(--border-main);
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form - Clean and accessible */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 11px 14px;
|
||||||
|
border: 2px solid var(--border-main);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
color: var(--text-main);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 28px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 2px solid var(--border-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading & Empty States */
|
||||||
|
.loading, .empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state svg {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
opacity: 0.4;
|
||||||
|
stroke: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
color: var(--text-main);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Buttons */
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons button {
|
||||||
|
padding: 7px 14px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import Zone - Visual and inviting */
|
||||||
|
.import-zone {
|
||||||
|
border: 3px dashed var(--border-main);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 48px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--bg-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-zone:hover, .import-zone.drag-over {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 0 20px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-zone.processing {
|
||||||
|
border-color: var(--success);
|
||||||
|
background: linear-gradient(135deg, #c6f6d5 0%, #9ae6b4 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-zone svg {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
stroke: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-zone h3 {
|
||||||
|
color: var(--text-main);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-zone p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import Results */
|
||||||
|
.import-results {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-results.success {
|
||||||
|
background: #c6f6d5;
|
||||||
|
border-color: var(--success);
|
||||||
|
color: #22543d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-results.error {
|
||||||
|
background: #fed7d7;
|
||||||
|
border-color: var(--danger);
|
||||||
|
color: #742a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress Bar - More visual */
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 10px;
|
||||||
|
background: var(--border-main);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--primary) 0%, var(--info) 100%);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
box-shadow: 0 0 10px rgba(102, 126, 234, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NEW STYLES FOR ENHANCED UI */
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
background-color: var(--danger);
|
||||||
|
color: white;
|
||||||
|
border-left-color: var(--danger-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card h3 {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-card {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-left-color: var(--primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card details {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card summary:hover {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flags-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flags-list li {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding-left: 16px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flags-list li::before {
|
||||||
|
content: '›';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value.level-high {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value.level-medium {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value.level-low {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
91
project-management/feedback/templates/admin/clients.html
Normal file
91
project-management/feedback/templates/admin/clients.html
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Client Administration</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>OIDC Clients</h1>
|
||||||
|
<p>Manage OIDC clients - Logged in as: <strong>{{ admin_user.username }}</strong></p>
|
||||||
|
</header>
|
||||||
|
{% if message %}
|
||||||
|
<div class="import-results success" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="controls">
|
||||||
|
<a href="/admin/client/create" style="text-decoration: none;">
|
||||||
|
<button class="secondary">Create New Client</button>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/users" style="text-decoration: none;">
|
||||||
|
<button>Manage Users</button>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/logout" style="text-decoration: none;">
|
||||||
|
<button class="danger">Logout</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Client ID</th>
|
||||||
|
<th>Client Name</th>
|
||||||
|
<th>Redirect URIs</th>
|
||||||
|
<th>Allowed Scopes</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for client in clients %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ client.id }}</strong></td>
|
||||||
|
<td><code>{{ client.client_id }}</code></td>
|
||||||
|
<td>{{ client.client_name }}</td>
|
||||||
|
<td>
|
||||||
|
<ul>
|
||||||
|
{% for uri in client.get_redirect_uris() %}
|
||||||
|
<li>{{ uri }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>{{ client.get_allowed_scopes()|join(', ') }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="/admin/client/{{ client.id }}/edit" style="text-decoration: none;">
|
||||||
|
<button type="button" style="padding: 7px 14px; font-size: 0.8rem;">Edit</button>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="/admin/client/{{ client.id }}/delete" style="display: inline;" onsubmit="return confirm('Delete client {{ client.client_name }}?');">
|
||||||
|
<button type="submit" class="danger" style="padding: 7px 14px; font-size: 0.8rem;">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Create New Client</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Create New OIDC Client</h1>
|
||||||
|
<p>Add a new client application to the system</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 600px; margin: 0 auto;">
|
||||||
|
{% if error %}
|
||||||
|
<div class="import-results error">
|
||||||
|
<strong>Error:</strong> {{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" style="margin-top: 24px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="client_name">Client Name</label>
|
||||||
|
<input type="text" id="client_name" name="client_name" placeholder="My Awesome App" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="client_id">Client ID</label>
|
||||||
|
<input type="text" id="client_id" name="client_id" placeholder="leave blank to auto-generate" >
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="client_secret">Client Secret</label>
|
||||||
|
<input type="text" id="client_secret" name="client_secret" placeholder="leave blank to auto-generate">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="redirect_uris">Redirect URIs (one per line)</label>
|
||||||
|
<textarea id="redirect_uris" name="redirect_uris" rows="3" placeholder="https://app.example.com/callback" required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="allowed_scopes">Allowed Scopes (comma-separated)</label>
|
||||||
|
<input type="text" id="allowed_scopes" name="allowed_scopes" value="openid, profile, email" placeholder="e.g. openid, profile, email">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="/admin/clients">
|
||||||
|
<button type="button" class="danger">Cancel</button>
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="secondary">Create Client</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
106
project-management/feedback/templates/admin/create_user.html
Normal file
106
project-management/feedback/templates/admin/create_user.html
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Create New User</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Create New User</h1>
|
||||||
|
<p>Add a new user to the system</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 600px; margin: 0 auto;">
|
||||||
|
{% if error %}
|
||||||
|
<div class="import-results error">
|
||||||
|
<strong>Error:</strong> {{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" style="margin-top: 24px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="Enter username" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" name="email" placeholder="user@example.com" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Full Name</label>
|
||||||
|
<input type="text" id="name" name="name" placeholder="John Doe" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" placeholder="Enter password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="role">Role</label>
|
||||||
|
<select id="role" name="role" required>
|
||||||
|
<option value="user" selected>User</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="moderator">Moderator</option>
|
||||||
|
<option value="readonly">Read-Only</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="permissions">Permissions (comma-separated)</label>
|
||||||
|
<input type="text" id="permissions" name="permissions" placeholder="e.g. read:data, write:data">
|
||||||
|
<small style="color: var(--text-secondary); display: block; margin-top: 8px;">
|
||||||
|
Common permissions: read:data, write:data, manage:users, manage:settings
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="is_admin">
|
||||||
|
Admin User
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="is_active" checked>
|
||||||
|
Account Active
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="/admin/users">
|
||||||
|
<button type="button" class="danger">Cancel</button>
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="secondary">Create User</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
158
project-management/feedback/templates/admin/dashboard.html
Normal file
158
project-management/feedback/templates/admin/dashboard.html
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>User Administration</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>User Administration</h1>
|
||||||
|
<p>Manage OIDC users - Logged in as: <strong>{{ admin_user.username }}</strong></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% if message %}
|
||||||
|
<div class="import-results success" style="max-width: 100%; margin-bottom: 20px;">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Total Users</h3>
|
||||||
|
<div class="value">{{ total_users }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Active Users</h3>
|
||||||
|
<div class="value value.level-low">{{ active_users }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Admin Users</h3>
|
||||||
|
<div class="value">{{ admin_users }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Inactive Users</h3>
|
||||||
|
<div class="value value.level-medium">{{ inactive_users }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<a href="/admin/analytics" style="text-decoration: none;">
|
||||||
|
<button>📊 Analytics</button>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/clients" style="text-decoration: none;">
|
||||||
|
<button>Manage Clients</button>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/user/create" style="text-decoration: none;">
|
||||||
|
<button class="secondary">Create New User</button>
|
||||||
|
</a>
|
||||||
|
<a href="/admin/logout" style="text-decoration: none;">
|
||||||
|
<button class="danger">Logout</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Permissions</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ user.id }}</strong></td>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.name }}</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>
|
||||||
|
{% if user.is_active %}
|
||||||
|
<span class="status-badge status-available">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-badge status-retired">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if user.role == 'admin' %}
|
||||||
|
<span class="status-badge status-in_use">{{ user.role|capitalize }}</span>
|
||||||
|
{% elif user.role == 'moderator' %}
|
||||||
|
<span class="status-badge status-available">{{ user.role|capitalize }}</span>
|
||||||
|
{% elif user.role == 'readonly' %}
|
||||||
|
<span class="status-badge status-retired">{{ user.role|capitalize }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-badge">{{ user.role|capitalize }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="font-size: 0.85rem;">
|
||||||
|
{% if user.get_permissions()|length > 0 %}
|
||||||
|
{{ user.get_permissions()|join(', ') }}
|
||||||
|
{% else %}
|
||||||
|
<em style="color: var(--text-secondary);">None</em>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="/admin/user/{{ user.id }}/edit" style="text-decoration: none;">
|
||||||
|
<button type="button" style="padding: 7px 14px; font-size: 0.8rem;">Edit</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% if user.is_active %}
|
||||||
|
<form method="POST" action="/admin/user/{{ user.id }}/deactivate" style="display: inline;">
|
||||||
|
<button type="submit" class="danger" style="padding: 7px 14px; font-size: 0.8rem;">Deactivate</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form method="POST" action="/admin/user/{{ user.id }}/activate" style="display: inline;">
|
||||||
|
<button type="submit" class="secondary" style="padding: 7px 14px; font-size: 0.8rem;">Activate</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not user.is_admin or admin_count > 1 %}
|
||||||
|
<form method="POST" action="/admin/user/{{ user.id }}/delete" style="display: inline;" onsubmit="return confirm('Delete user {{ user.username }}?');">
|
||||||
|
<button type="submit" class="danger" style="padding: 7px 14px; font-size: 0.8rem;">Delete</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="/admin/user/{{ user.id }}/tokens" style="text-decoration: none;">
|
||||||
|
<button type="button" class="secondary" style="padding: 7px 14px; font-size: 0.8rem;">Tokens</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
project-management/feedback/templates/admin/edit_client.html
Normal file
79
project-management/feedback/templates/admin/edit_client.html
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Edit Client - {{ client.client_name }}</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Edit OIDC Client</h1>
|
||||||
|
<p>Modify details for client: <strong>{{ client.client_name }}</strong></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 600px; margin: 0 auto;">
|
||||||
|
{% if error %}
|
||||||
|
<div class="import-results error">
|
||||||
|
<strong>Error:</strong> {{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" style="margin-top: 24px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="client_name">Client Name</label>
|
||||||
|
<input type="text" id="client_name" name="client_name" value="{{ client.client_name }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="client_id">Client ID</label>
|
||||||
|
<input type="text" id="client_id" name="client_id" value="{{ client.client_id }}" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_client_secret">New Client Secret (leave empty to keep current)</label>
|
||||||
|
<input type="text" id="new_client_secret" name="new_client_secret" placeholder="Optional: Set new secret">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="redirect_uris">Redirect URIs (one per line)</label>
|
||||||
|
<textarea id="redirect_uris" name="redirect_uris" rows="3" required>{{ client.get_redirect_uris()|join('\n') }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="allowed_scopes">Allowed Scopes (comma-separated)</label>
|
||||||
|
<input type="text" id="allowed_scopes" name="allowed_scopes" value="{{ client.get_allowed_scopes()|join(', ') }}" placeholder="e.g. openid, profile, email">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="/admin/clients">
|
||||||
|
<button type="button" class="danger">Cancel</button>
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="secondary">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
106
project-management/feedback/templates/admin/edit_user.html
Normal file
106
project-management/feedback/templates/admin/edit_user.html
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Edit User - {{ user.username }}</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Edit User</h1>
|
||||||
|
<p>Modify user details for: <strong>{{ user.username }}</strong></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 600px; margin: 0 auto;">
|
||||||
|
{% if error %}
|
||||||
|
<div class="import-results error">
|
||||||
|
<strong>Error:</strong> {{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" style="margin-top: 24px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" value="{{ user.username }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" id="email" name="email" value="{{ user.email }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Full Name</label>
|
||||||
|
<input type="text" id="name" name="name" value="{{ user.name }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="is_admin" {% if user.is_admin %}checked{% endif %}>
|
||||||
|
Admin User
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="is_active" {% if user.is_active %}checked{% endif %}>
|
||||||
|
Account Active
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="role">Role</label>
|
||||||
|
<select id="role" name="role" required>
|
||||||
|
<option value="user" {% if user.role == 'user' %}selected{% endif %}>User</option>
|
||||||
|
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>Admin</option>
|
||||||
|
<option value="moderator" {% if user.role == 'moderator' %}selected{% endif %}>Moderator</option>
|
||||||
|
<option value="readonly" {% if user.role == 'readonly' %}selected{% endif %}>Read-Only</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="permissions">Permissions (comma-separated)</label>
|
||||||
|
<input type="text" id="permissions" name="permissions" value="{{ user.get_permissions()|join(', ') }}" placeholder="e.g. read:data, write:data, manage:users">
|
||||||
|
<small style="color: var(--text-secondary); display: block; margin-top: 8px;">
|
||||||
|
Common permissions: read:data, write:data, manage:users, manage:settings
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password">New Password (leave empty to keep current)</label>
|
||||||
|
<input type="password" id="new_password" name="new_password" placeholder="Optional: Set new password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="/admin/users">
|
||||||
|
<button type="button" class="danger">Cancel</button>
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="secondary">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
63
project-management/feedback/templates/admin/login.html
Normal file
63
project-management/feedback/templates/admin/login.html
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Login</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Admin Login</h1>
|
||||||
|
<p>User Administration Access</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 450px; margin: 0 auto;">
|
||||||
|
{% if error %}
|
||||||
|
<div class="import-results error">
|
||||||
|
<strong>Error:</strong> {{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" style="margin-top: 24px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Admin Username</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="Enter admin username" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" placeholder="Enter password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" style="width: 100%; margin-top: 8px;">Admin Login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 24px; padding-top: 24px; border-top: 2px solid var(--border-main);">
|
||||||
|
<a href="/" style="color: var(--primary); text-decoration: none; font-weight: 600;">← Back to Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
99
project-management/feedback/templates/admin/user_tokens.html
Normal file
99
project-management/feedback/templates/admin/user_tokens.html
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>User Tokens</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Access Tokens for {{ target_user.username }}</h1>
|
||||||
|
<p>Logged in as admin: <strong>{{ admin_user.username }}</strong></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% if message %}
|
||||||
|
<div class="import-results success" style="max-width: 100%; margin-bottom: 16px;">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if new_token %}
|
||||||
|
<div class="import-results warning" style="max-width: 100%; margin-bottom: 16px; word-break: break-all;">
|
||||||
|
New Token (copy now): <code>{{ new_token }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="import-results" style="background: #3b1a1a; color: #f0b6b6; max-width: 100%; margin-bottom: 16px;">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 640px; margin: 0 auto;">
|
||||||
|
<h2>Create Initial Access Token</h2>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Scope</label>
|
||||||
|
<input type="text" name="scope" value="dcr:register">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>TTL (days)</label>
|
||||||
|
<input type="number" name="ttl_days" value="365" min="1" max="3650">
|
||||||
|
</div>
|
||||||
|
<button type="submit">Create Token</button>
|
||||||
|
<a href="/admin/users" style="margin-left: 8px;">Back</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container" style="margin-top: 24px;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Token</th>
|
||||||
|
<th>Scope</th>
|
||||||
|
<th>Expires</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for token in tokens %}
|
||||||
|
<tr>
|
||||||
|
<td style="max-width: 320px; word-break: break-all;"><code>{{ token.token }}</code></td>
|
||||||
|
<td>{{ token.scope }}</td>
|
||||||
|
<td>{{ token.expires_at.strftime('%Y-%m-%d') }}</td>
|
||||||
|
<td>
|
||||||
|
{% if token.revoked %}
|
||||||
|
<span class="status-badge status-retired">Revoked</span>
|
||||||
|
{% elif token.is_valid %}
|
||||||
|
<span class="status-badge status-available">Valid</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-badge status-retired">Expired</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="/admin/user/{{ target_user.id }}/tokens/{{ token.id }}" style="display: inline-block; margin-right: 6px;">
|
||||||
|
<input type="hidden" name="action" value="revoke">
|
||||||
|
<button type="submit" class="danger" style="padding: 6px 10px; font-size: 0.8rem;">Revoke</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/admin/user/{{ target_user.id }}/tokens/{{ token.id }}" style="display: inline-block; margin-right: 6px;">
|
||||||
|
<input type="hidden" name="action" value="update">
|
||||||
|
<input type="text" name="scope" value="{{ token.scope }}" style="width: 140px; font-size: 0.8rem;" aria-label="Scope">
|
||||||
|
<input type="number" name="ttl_days" value="365" min="1" max="3650" style="width: 70px; font-size: 0.8rem;" aria-label="TTL days">
|
||||||
|
<button type="submit" class="secondary" style="padding: 6px 10px; font-size: 0.8rem;">Update</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/admin/user/{{ target_user.id }}/tokens/{{ token.id }}" style="display: inline-block;">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<button type="submit" class="danger" style="padding: 6px 10px; font-size: 0.8rem;" onclick="return confirm('Delete this token?');">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
project-management/feedback/templates/change_password.html
Normal file
79
project-management/feedback/templates/change_password.html
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OIDC IdP - Change Password</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Change Password</h1>
|
||||||
|
<p>Update your account security</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 500px; margin: 0 auto;">
|
||||||
|
{% if error %}
|
||||||
|
<div class="import-results error">
|
||||||
|
<strong>Error:</strong> {{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if success %}
|
||||||
|
<div class="import-results success">
|
||||||
|
<strong>Success:</strong> {{ success }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" style="margin-top: 24px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="Your username" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="current_password">Current Password</label>
|
||||||
|
<input type="password" id="current_password" name="current_password" placeholder="Enter current password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password">New Password</label>
|
||||||
|
<input type="password" id="new_password" name="new_password" placeholder="Min. 8 characters" required minlength="8">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password_confirm">Confirm New Password</label>
|
||||||
|
<input type="password" id="new_password_confirm" name="new_password_confirm" placeholder="Repeat new password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" style="width: 100%; margin-top: 8px;">Update Password</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 24px; padding-top: 24px; border-top: 2px solid var(--border-main);">
|
||||||
|
<a href="/" style="color: var(--primary); text-decoration: none; font-weight: 600;">← Back to Login</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
86
project-management/feedback/templates/dashboard.html
Normal file
86
project-management/feedback/templates/dashboard.html
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>User Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>👤 User Dashboard</h1>
|
||||||
|
<p>Logged in as: <strong>{{ user.username }}</strong></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2 style="color: var(--text-main); margin-bottom: 20px;">Your Information</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" value="{{ user.username }}" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Email</label>
|
||||||
|
<input type="text" value="{{ user.email }}" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Full Name</label>
|
||||||
|
<input type="text" value="{{ user.name }}" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Role</label>
|
||||||
|
<input type="text" value="{{ user.role }}" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Permissions</label>
|
||||||
|
<input type="text" value="{{ user.get_permissions()|join(', ') }}" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Account Status</label>
|
||||||
|
<input type="text" value="{% if user.is_active %}Active{% else %}Inactive{% endif %}" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls" style="margin-top: 24px;">
|
||||||
|
<a href="/my-sessions" style="text-decoration: none;">
|
||||||
|
<button>📊 My Sessions</button>
|
||||||
|
</a>
|
||||||
|
<a href="/change-password" style="text-decoration: none;">
|
||||||
|
<button>🔑 Change Password</button>
|
||||||
|
</a>
|
||||||
|
<a href="/my-tokens" style="text-decoration: none;">
|
||||||
|
<button>🎟️ My Tokens</button>
|
||||||
|
</a>
|
||||||
|
<a href="/logout" style="text-decoration: none;">
|
||||||
|
<button class="danger">Logout</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
60
project-management/feedback/templates/index.html
Normal file
60
project-management/feedback/templates/index.html
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OIDC Identity Provider</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>🔐 OIDC Identity Provider</h1>
|
||||||
|
<p>Secure authentication server for your services</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 700px; margin: 0 auto;">
|
||||||
|
<h2 style="color: var(--text-main); margin-bottom: 20px;">Welcome</h2>
|
||||||
|
<p style="color: var(--text-secondary); line-height: 1.6;">
|
||||||
|
This is an OpenID Connect (OIDC) Identity Provider that enables secure authentication
|
||||||
|
for your applications using industry-standard protocols.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls" style="margin-top: 32px; justify-content: center;">
|
||||||
|
<a href="/login" style="text-decoration: none;">
|
||||||
|
<button>🔑 Login</button>
|
||||||
|
</a>
|
||||||
|
<a href="/register" style="text-decoration: none;">
|
||||||
|
<button class="secondary">📝 Register</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 24px; padding-top: 24px; border-top: 2px solid var(--border-main);">
|
||||||
|
<p style="color: var(--text-secondary); font-size: 0.9rem;">
|
||||||
|
Administrators: <a href="/admin/login" style="color: var(--primary); text-decoration: none;">Access admin panel</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
71
project-management/feedback/templates/login.html
Normal file
71
project-management/feedback/templates/login.html
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OIDC IdP - Login</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>🔐 Homelab OIDC Login</h1>
|
||||||
|
<p>Secure authentication for your homelab services</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 450px; margin: 0 auto;">
|
||||||
|
{% if error %}
|
||||||
|
<div class="import-results error">
|
||||||
|
<strong>Error:</strong> {{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if success %}
|
||||||
|
<div class="import-results success">
|
||||||
|
<strong>Success:</strong> {{ success }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" style="margin-top: 24px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="Enter your username" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" placeholder="Enter your password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" style="width: 100%; margin-top: 8px;">Sign In</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 24px; padding-top: 24px; border-top: 2px solid var(--border-main);">
|
||||||
|
<p style="color: var(--text-secondary); margin-bottom: 12px;">Don't have an account?</p>
|
||||||
|
<a href="/register" style="color: var(--primary); text-decoration: none; font-weight: 600;">Create new account →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load saved theme
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
project-management/feedback/templates/register.html
Normal file
79
project-management/feedback/templates/register.html
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OIDC IdP - Registration</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Create New Account</h1>
|
||||||
|
<p>Join your homelab authentication system</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 500px; margin: 0 auto;">
|
||||||
|
{% if error %}
|
||||||
|
<div class="import-results error">
|
||||||
|
<strong>Error:</strong> {{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" style="margin-top: 24px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="Choose a username" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email Address</label>
|
||||||
|
<input type="email" id="email" name="email" placeholder="your.email@homelab.local" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Full Name</label>
|
||||||
|
<input type="text" id="name" name="name" placeholder="John Doe" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" placeholder="Min. 8 characters" required minlength="8">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password_confirm">Confirm Password</label>
|
||||||
|
<input type="password" id="password_confirm" name="password_confirm" placeholder="Repeat your password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="secondary" style="width: 100%; margin-top: 8px;">Create Account</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 24px; padding-top: 24px; border-top: 2px solid var(--border-main);">
|
||||||
|
<p style="color: var(--text-secondary); margin-bottom: 12px;">Already have an account?</p>
|
||||||
|
<a href="/" style="color: var(--primary); text-decoration: none; font-weight: 600;">← Back to Login</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
103
project-management/feedback/templates/user_analytics.html
Normal file
103
project-management/feedback/templates/user_analytics.html
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>My Active Sessions</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
||||||
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>My Active Sessions</h1>
|
||||||
|
<p>{{ user.name }} ({{ user.email }})</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 24px;">
|
||||||
|
<a href="/dashboard" style="text-decoration: none;">
|
||||||
|
<button>Back to Dashboard</button>
|
||||||
|
</a>
|
||||||
|
<a href="/logout" style="text-decoration: none;">
|
||||||
|
<button class="danger">Logout</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary Stats -->
|
||||||
|
<div class="analytics-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 32px;">
|
||||||
|
<div class="analytics-card" style="background: var(--bg-secondary); padding: 24px; border-radius: 8px;">
|
||||||
|
<h3 style="font-size: 14px; color: var(--text-secondary); margin-bottom: 8px;">Active Sessions</h3>
|
||||||
|
<div class="metric" style="font-size: 32px; font-weight: 600; color: var(--primary-color);">{{ summary.total_active_sessions }}</div>
|
||||||
|
<div class="label" style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">Currently active</div>
|
||||||
|
</div>
|
||||||
|
<div class="analytics-card" style="background: var(--bg-secondary); padding: 24px; border-radius: 8px;">
|
||||||
|
<h3 style="font-size: 14px; color: var(--text-secondary); margin-bottom: 8px;">Applications</h3>
|
||||||
|
<div class="metric" style="font-size: 32px; font-weight: 600; color: var(--primary-color);">{{ summary.total_clients }}</div>
|
||||||
|
<div class="label" style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">You're using</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Sessions -->
|
||||||
|
<h2 style="margin-bottom: 20px;">Active Sessions</h2>
|
||||||
|
|
||||||
|
{% if active_sessions %}
|
||||||
|
{% set current_client = namespace(value='') %}
|
||||||
|
{% for session in active_sessions %}
|
||||||
|
{% if session.client_name != current_client.value %}
|
||||||
|
{% set current_client.value = session.client_name %}
|
||||||
|
{% if not loop.first %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="client-section" style="background: var(--bg-secondary); padding: 24px; border-radius: 8px; margin-bottom: 16px;">
|
||||||
|
<h3 style="margin-bottom: 16px;">{{ session.client_name }}</h3>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="session-item" style="padding: 16px; background: var(--bg-primary); border-radius: 6px; margin-bottom: 12px;">
|
||||||
|
<div class="session-info" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||||
|
<div>
|
||||||
|
<strong>Session</strong>
|
||||||
|
</div>
|
||||||
|
<span class="status-badge status-available" style="padding: 4px 12px; background: #10b981; color: white; border-radius: 4px; font-size: 12px;">Active</span>
|
||||||
|
</div>
|
||||||
|
<div class="session-meta" style="font-size: 14px; color: var(--text-secondary);">
|
||||||
|
Created: {{ session.created_at.strftime('%Y-%m-%d %H:%M:%S') }} |
|
||||||
|
Expires: {{ session.expires_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if loop.last %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="import-results" style="background: var(--bg-secondary); padding: 20px; border-radius: 8px;">
|
||||||
|
No active sessions. Log in to an application to see sessions here.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('darkMode') === 'true') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
location.reload();
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
99
project-management/feedback/templates/user_tokens.html
Normal file
99
project-management/feedback/templates/user_tokens.html
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>My Tokens</title>
|
||||||
|
<link rel="stylesheet" href="/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>Initial Access Tokens</h1>
|
||||||
|
<p>Logged in as: <strong>{{ user.username }}</strong></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% if message %}
|
||||||
|
<div class="import-results success" style="max-width: 100%; margin-bottom: 16px;">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if new_token %}
|
||||||
|
<div class="import-results warning" style="max-width: 100%; margin-bottom: 16px; word-break: break-all;">
|
||||||
|
New Token (copy now): <code>{{ new_token }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="import-results" style="background: #3b1a1a; color: #f0b6b6; max-width: 100%; margin-bottom: 16px;">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="modal-content" style="max-width: 640px; margin: 0 auto;">
|
||||||
|
<h2>Create Initial Access Token</h2>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Scope</label>
|
||||||
|
<input type="text" name="scope" value="dcr:register">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>TTL (days)</label>
|
||||||
|
<input type="number" name="ttl_days" value="365" min="1" max="3650">
|
||||||
|
</div>
|
||||||
|
<button type="submit">Create Token</button>
|
||||||
|
<a href="/dashboard" style="margin-left: 8px;">Back</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container" style="margin-top: 24px;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Token</th>
|
||||||
|
<th>Scope</th>
|
||||||
|
<th>Expires</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for token in tokens %}
|
||||||
|
<tr>
|
||||||
|
<td style="max-width: 320px; word-break: break-all;"><code>{{ token.token }}</code></td>
|
||||||
|
<td>{{ token.scope }}</td>
|
||||||
|
<td>{{ token.expires_at.strftime('%Y-%m-%d') }}</td>
|
||||||
|
<td>
|
||||||
|
{% if token.revoked %}
|
||||||
|
<span class="status-badge status-retired">Revoked</span>
|
||||||
|
{% elif token.is_valid %}
|
||||||
|
<span class="status-badge status-available">Valid</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-badge status-retired">Expired</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="/my-tokens/{{ token.id }}" style="display: inline-block; margin-right: 6px;">
|
||||||
|
<input type="hidden" name="action" value="revoke">
|
||||||
|
<button type="submit" class="danger" style="padding: 6px 10px; font-size: 0.8rem;">Revoke</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/my-tokens/{{ token.id }}" style="display: inline-block; margin-right: 6px;">
|
||||||
|
<input type="hidden" name="action" value="update">
|
||||||
|
<input type="text" name="scope" value="{{ token.scope }}" style="width: 140px; font-size: 0.8rem;" aria-label="Scope">
|
||||||
|
<input type="number" name="ttl_days" value="365" min="1" max="3650" style="width: 70px; font-size: 0.8rem;" aria-label="TTL days">
|
||||||
|
<button type="submit" class="secondary" style="padding: 6px 10px; font-size: 0.8rem;">Update</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/my-tokens/{{ token.id }}" style="display: inline-block;">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<button type="submit" class="danger" style="padding: 6px 10px; font-size: 0.8rem;" onclick="return confirm('Delete this token?');">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
project-management/requirements/tasks/TASK_000036.md
Normal file
20
project-management/requirements/tasks/TASK_000036.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
ID: TASK_000036 | Version: 0.1.2 | Status: Done
|
||||||
|
By: Codex (GPT-5)
|
||||||
|
|
||||||
|
# TASK_000036: Login-Landing und Dashboard-Redirect
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
Beim Aufruf der URL wird eine Login-Landing-Page gezeigt; nach erfolgreicher Anmeldung erfolgt die Weiterleitung auf das Dashboard unter eigener URL.
|
||||||
|
|
||||||
|
## Story-Bezug
|
||||||
|
US_000022
|
||||||
|
|
||||||
|
## Beschreibung
|
||||||
|
- Zusaetzliche Routen `/login` und `/dashboard` liefern die Web-UI.
|
||||||
|
- Nicht-authentifizierte Zugriffe auf `/dashboard` wechseln auf `/login`.
|
||||||
|
- Nach erfolgreichem Login wird die URL auf `/dashboard` gesetzt.
|
||||||
|
|
||||||
|
## Definition of Done (DoD)
|
||||||
|
- Login-Landing ist Standardansicht ohne aktive Session.
|
||||||
|
- Nach Login wird `/dashboard` angezeigt.
|
||||||
|
- `/login` und `/dashboard` laden die UI.
|
||||||
Reference in New Issue
Block a user