A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1406 lines
44 KiB
Python
1406 lines
44 KiB
Python
"""
|
|
Auth Modal Component - Login, Register, 2FA
|
|
"""
|
|
|
|
def get_auth_modal_css() -> str:
|
|
"""CSS für Auth Modal zurückgeben"""
|
|
return """
|
|
/* ==========================================
|
|
AUTH MODAL STYLES
|
|
========================================== */
|
|
.auth-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 10000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.auth-modal.active {
|
|
display: flex;
|
|
}
|
|
|
|
.auth-modal-content {
|
|
background: var(--bp-surface);
|
|
border-radius: 16px;
|
|
width: 90%;
|
|
max-width: 420px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
|
border: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.auth-modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 20px 24px;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.auth-modal-header h2 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.auth-tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
padding: 12px 24px;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
background: var(--bp-surface-elevated);
|
|
}
|
|
|
|
.auth-tab {
|
|
flex: 1;
|
|
padding: 10px 16px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--bp-text-muted);
|
|
cursor: pointer;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.auth-tab:hover {
|
|
background: var(--bp-border-subtle);
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.auth-tab.active {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
}
|
|
|
|
.auth-body {
|
|
padding: 24px;
|
|
}
|
|
|
|
.auth-content {
|
|
display: none;
|
|
}
|
|
|
|
.auth-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.auth-form-group {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.auth-form-label {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.auth-form-input {
|
|
width: 100%;
|
|
padding: 10px 14px;
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 8px;
|
|
background: var(--bp-surface-elevated);
|
|
color: var(--bp-text);
|
|
font-size: 14px;
|
|
transition: border-color 0.2s ease;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.auth-form-input:focus {
|
|
outline: none;
|
|
border-color: var(--bp-primary);
|
|
}
|
|
|
|
.auth-form-input::placeholder {
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.auth-error {
|
|
display: none;
|
|
padding: 10px 14px;
|
|
background: rgba(220, 53, 69, 0.15);
|
|
border: 1px solid var(--bp-danger);
|
|
border-radius: 8px;
|
|
color: var(--bp-danger);
|
|
font-size: 13px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.auth-error.active {
|
|
display: block;
|
|
}
|
|
|
|
.auth-success {
|
|
display: none;
|
|
padding: 10px 14px;
|
|
background: rgba(40, 167, 69, 0.15);
|
|
border: 1px solid var(--bp-success);
|
|
border-radius: 8px;
|
|
color: var(--bp-success);
|
|
font-size: 13px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.auth-success.active {
|
|
display: block;
|
|
}
|
|
|
|
.auth-btn {
|
|
width: 100%;
|
|
padding: 12px 20px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.auth-btn-primary {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
}
|
|
|
|
.auth-btn-primary:hover {
|
|
filter: brightness(1.1);
|
|
}
|
|
|
|
.auth-btn-primary:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.auth-link {
|
|
text-align: center;
|
|
margin-top: 16px;
|
|
font-size: 13px;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.auth-link a {
|
|
color: var(--bp-primary);
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.auth-link a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.auth-divider {
|
|
display: flex;
|
|
align-items: center;
|
|
text-align: center;
|
|
margin: 20px 0;
|
|
color: var(--bp-text-muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.auth-divider::before,
|
|
.auth-divider::after {
|
|
content: '';
|
|
flex: 1;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.auth-divider::before {
|
|
margin-right: 12px;
|
|
}
|
|
|
|
.auth-divider::after {
|
|
margin-left: 12px;
|
|
}
|
|
|
|
.auth-user-dropdown {
|
|
position: relative;
|
|
display: none;
|
|
}
|
|
|
|
.auth-user-dropdown.active {
|
|
display: flex;
|
|
}
|
|
|
|
.auth-user-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 12px;
|
|
background: var(--bp-surface-elevated);
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 8px;
|
|
color: var(--bp-text);
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.auth-user-btn:hover {
|
|
border-color: var(--bp-primary);
|
|
}
|
|
|
|
.auth-user-avatar {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 50%;
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.auth-user-menu {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
margin-top: 8px;
|
|
background: var(--bp-surface);
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 8px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
min-width: 200px;
|
|
z-index: 1000;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.auth-user-menu.active {
|
|
display: block;
|
|
}
|
|
|
|
.auth-user-menu-header {
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.auth-user-menu-name {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.auth-user-menu-email {
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.auth-user-menu-item {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 10px 16px;
|
|
border: none;
|
|
background: none;
|
|
text-align: left;
|
|
color: var(--bp-text);
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.auth-user-menu-item:hover {
|
|
background: var(--bp-surface-elevated);
|
|
}
|
|
|
|
.auth-user-menu-item.danger {
|
|
color: var(--bp-danger);
|
|
}
|
|
|
|
[data-theme="light"] .auth-modal-content {
|
|
background: #FFFFFF;
|
|
border-color: #E0E0E0;
|
|
}
|
|
|
|
[data-theme="light"] .auth-tabs {
|
|
background: #F5F5F5;
|
|
}
|
|
|
|
[data-theme="light"] .auth-form-input {
|
|
background: #FFFFFF;
|
|
border-color: #E0E0E0;
|
|
}
|
|
|
|
[data-theme="light"] .auth-user-menu {
|
|
background: #FFFFFF;
|
|
border-color: #E0E0E0;
|
|
}
|
|
|
|
/* ==========================================
|
|
NOTIFICATION STYLES
|
|
========================================== */
|
|
.notification-bell {
|
|
position: relative;
|
|
display: none;
|
|
}
|
|
|
|
.notification-bell.active {
|
|
display: flex;
|
|
}
|
|
|
|
.notification-bell-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 40px;
|
|
height: 40px;
|
|
background: var(--bp-surface-elevated);
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 8px;
|
|
color: var(--bp-text-muted);
|
|
cursor: pointer;
|
|
font-size: 18px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.notification-bell-btn:hover {
|
|
border-color: var(--bp-primary);
|
|
color: var(--bp-primary);
|
|
}
|
|
|
|
.notification-badge {
|
|
position: absolute;
|
|
top: 2px;
|
|
right: 2px;
|
|
min-width: 18px;
|
|
height: 18px;
|
|
background: var(--bp-danger);
|
|
color: white;
|
|
border-radius: 999px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0 5px;
|
|
}
|
|
|
|
.notification-badge.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.notification-panel {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
margin-top: 8px;
|
|
width: 360px;
|
|
max-height: 480px;
|
|
background: var(--bp-surface);
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
|
z-index: 1000;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.notification-panel.active {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.notification-panel-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.notification-panel-title {
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.notification-panel-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.notification-panel-action {
|
|
padding: 4px 8px;
|
|
background: none;
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 6px;
|
|
color: var(--bp-text-muted);
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.notification-panel-action:hover {
|
|
border-color: var(--bp-primary);
|
|
color: var(--bp-primary);
|
|
}
|
|
|
|
.notification-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
max-height: 380px;
|
|
}
|
|
|
|
.notification-item {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
cursor: pointer;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.notification-item:hover {
|
|
background: var(--bp-surface-elevated);
|
|
}
|
|
|
|
.notification-item.unread {
|
|
background: rgba(15, 118, 110, 0.08);
|
|
}
|
|
|
|
.notification-item.unread::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 3px;
|
|
background: var(--bp-primary);
|
|
}
|
|
|
|
.notification-item {
|
|
position: relative;
|
|
}
|
|
|
|
.notification-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
background: var(--bp-primary-soft);
|
|
color: var(--bp-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.notification-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.notification-title {
|
|
font-weight: 500;
|
|
font-size: 13px;
|
|
color: var(--bp-text);
|
|
margin-bottom: 4px;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.notification-body {
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
line-height: 1.4;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
.notification-time {
|
|
font-size: 11px;
|
|
color: var(--bp-text-muted);
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.notification-empty {
|
|
padding: 40px 20px;
|
|
text-align: center;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.notification-empty-icon {
|
|
font-size: 36px;
|
|
margin-bottom: 12px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.notification-footer {
|
|
padding: 12px 16px;
|
|
border-top: 1px solid var(--bp-border);
|
|
text-align: center;
|
|
}
|
|
|
|
.notification-footer-btn {
|
|
background: none;
|
|
border: none;
|
|
color: var(--bp-primary);
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.notification-footer-btn:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Notification Preferences Modal */
|
|
.notification-prefs-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 10001;
|
|
justify-content: center;
|
|
align-items: center;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.notification-prefs-modal.active {
|
|
display: flex;
|
|
}
|
|
|
|
.notification-prefs-content {
|
|
background: var(--bp-surface);
|
|
border-radius: 16px;
|
|
width: 90%;
|
|
max-width: 420px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
|
border: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.notification-pref-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 14px 0;
|
|
border-bottom: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.notification-pref-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.notification-pref-label {
|
|
font-size: 14px;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.notification-pref-desc {
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.toggle-switch {
|
|
position: relative;
|
|
width: 48px;
|
|
height: 26px;
|
|
background: var(--bp-border);
|
|
border-radius: 999px;
|
|
cursor: pointer;
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
.toggle-switch.active {
|
|
background: var(--bp-primary);
|
|
}
|
|
|
|
.toggle-switch-handle {
|
|
position: absolute;
|
|
top: 3px;
|
|
left: 3px;
|
|
width: 20px;
|
|
height: 20px;
|
|
background: white;
|
|
border-radius: 50%;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.toggle-switch.active .toggle-switch-handle {
|
|
transform: translateX(22px);
|
|
}
|
|
|
|
[data-theme="light"] .notification-panel {
|
|
background: #FFFFFF;
|
|
border-color: #E0E0E0;
|
|
}
|
|
|
|
[data-theme="light"] .notification-item.unread {
|
|
background: rgba(108, 27, 27, 0.05);
|
|
}
|
|
|
|
[data-theme="light"] .notification-item.unread::before {
|
|
background: var(--bp-primary);
|
|
}
|
|
|
|
/* ==========================================
|
|
SUSPENSION OVERLAY STYLES
|
|
========================================== */
|
|
.suspension-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.9);
|
|
z-index: 20000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
.suspension-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.suspension-content {
|
|
background: var(--bp-surface);
|
|
border-radius: 20px;
|
|
width: 90%;
|
|
max-width: 500px;
|
|
padding: 40px;
|
|
text-align: center;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
|
border: 1px solid var(--bp-danger);
|
|
}
|
|
|
|
.suspension-icon {
|
|
font-size: 64px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.suspension-title {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: var(--bp-danger);
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.suspension-message {
|
|
font-size: 15px;
|
|
color: var(--bp-text-muted);
|
|
margin-bottom: 24px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.suspension-docs {
|
|
background: var(--bp-surface-elevated);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
margin-bottom: 24px;
|
|
text-align: left;
|
|
}
|
|
|
|
.suspension-docs-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--bp-text);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.suspension-doc-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px 12px;
|
|
background: var(--bp-bg);
|
|
border-radius: 8px;
|
|
margin-bottom: 8px;
|
|
border: 1px solid var(--bp-border);
|
|
}
|
|
|
|
.suspension-doc-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.suspension-doc-name {
|
|
font-size: 14px;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.suspension-doc-deadline {
|
|
font-size: 12px;
|
|
color: var(--bp-danger);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.suspension-btn {
|
|
padding: 14px 28px;
|
|
background: var(--bp-btn-primary-bg);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
width: 100%;
|
|
}
|
|
|
|
.suspension-btn:hover {
|
|
background: var(--bp-btn-primary-hover);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.suspension-countdown {
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
margin-top: 16px;
|
|
}
|
|
"""
|
|
|
|
|
|
def get_auth_modal_html() -> str:
|
|
"""HTML für Auth Modal zurückgeben"""
|
|
return """
|
|
<!-- Auth Modal -->
|
|
<div id="auth-modal" class="auth-modal">
|
|
<div class="auth-modal-content">
|
|
<div class="auth-modal-header">
|
|
<h2><span>🔐</span> Anmeldung</h2>
|
|
<button id="auth-modal-close" class="legal-modal-close">×</button>
|
|
</div>
|
|
<div class="auth-tabs">
|
|
<button class="auth-tab active" data-tab="login">Anmelden</button>
|
|
<button class="auth-tab" data-tab="register">Registrieren</button>
|
|
</div>
|
|
<div class="auth-body">
|
|
<!-- Login Tab -->
|
|
<div id="auth-login" class="auth-content active">
|
|
<div id="auth-login-error" class="auth-error"></div>
|
|
<div id="auth-login-success" class="auth-success"></div>
|
|
<form id="auth-login-form">
|
|
<div class="auth-form-group">
|
|
<label class="auth-form-label">E-Mail</label>
|
|
<input type="email" class="auth-form-input" id="login-email" placeholder="ihre@email.de" required>
|
|
</div>
|
|
<div class="auth-form-group">
|
|
<label class="auth-form-label">Passwort</label>
|
|
<input type="password" class="auth-form-input" id="login-password" placeholder="Ihr Passwort" required>
|
|
</div>
|
|
<button type="submit" class="auth-btn auth-btn-primary" id="login-btn">Anmelden</button>
|
|
</form>
|
|
<div class="auth-link">
|
|
<a href="#" id="auth-forgot-password">Passwort vergessen?</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Register Tab -->
|
|
<div id="auth-register" class="auth-content">
|
|
<div id="auth-register-error" class="auth-error"></div>
|
|
<div id="auth-register-success" class="auth-success"></div>
|
|
<form id="auth-register-form">
|
|
<div class="auth-form-group">
|
|
<label class="auth-form-label">Name (optional)</label>
|
|
<input type="text" class="auth-form-input" id="register-name" placeholder="Ihr Name">
|
|
</div>
|
|
<div class="auth-form-group">
|
|
<label class="auth-form-label">E-Mail</label>
|
|
<input type="email" class="auth-form-input" id="register-email" placeholder="ihre@email.de" required>
|
|
</div>
|
|
<div class="auth-form-group">
|
|
<label class="auth-form-label">Passwort</label>
|
|
<input type="password" class="auth-form-input" id="register-password" placeholder="Mind. 8 Zeichen" required minlength="8">
|
|
</div>
|
|
<div class="auth-form-group">
|
|
<label class="auth-form-label">Passwort bestätigen</label>
|
|
<input type="password" class="auth-form-input" id="register-password-confirm" placeholder="Passwort wiederholen" required>
|
|
</div>
|
|
<button type="submit" class="auth-btn auth-btn-primary" id="register-btn">Registrieren</button>
|
|
</form>
|
|
<div class="auth-link">
|
|
Bereits registriert? <a href="#" id="auth-goto-login">Hier anmelden</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Forgot Password Tab (hidden by default) -->
|
|
<div id="auth-forgot" class="auth-content">
|
|
<div id="auth-forgot-error" class="auth-error"></div>
|
|
<div id="auth-forgot-success" class="auth-success"></div>
|
|
<p style="color: var(--bp-text-muted); font-size: 13px; margin-bottom: 16px;">
|
|
Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts.
|
|
</p>
|
|
<form id="auth-forgot-form">
|
|
<div class="auth-form-group">
|
|
<label class="auth-form-label">E-Mail</label>
|
|
<input type="email" class="auth-form-input" id="forgot-email" placeholder="ihre@email.de" required>
|
|
</div>
|
|
<button type="submit" class="auth-btn auth-btn-primary" id="forgot-btn">Link senden</button>
|
|
</form>
|
|
<div class="auth-link">
|
|
<a href="#" id="auth-back-to-login">Zurück zur Anmeldung</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reset Password Tab (hidden by default, shown via URL token) -->
|
|
<div id="auth-reset" class="auth-content">
|
|
<div id="auth-reset-error" class="auth-error"></div>
|
|
<div id="auth-reset-success" class="auth-success"></div>
|
|
<form id="auth-reset-form">
|
|
<div class="auth-form-group">
|
|
<label class="auth-form-label">Neues Passwort</label>
|
|
<input type="password" class="auth-form-input" id="reset-password" placeholder="Mind. 8 Zeichen" required minlength="8">
|
|
</div>
|
|
<div class="auth-form-group">
|
|
<label class="auth-form-label">Passwort bestätigen</label>
|
|
<input type="password" class="auth-form-input" id="reset-password-confirm" placeholder="Passwort wiederholen" required>
|
|
</div>
|
|
<input type="hidden" id="reset-token">
|
|
<button type="submit" class="auth-btn auth-btn-primary" id="reset-btn">Passwort ändern</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Email Verification Tab (hidden by default, shown via URL token) -->
|
|
<div id="auth-verify" class="auth-content">
|
|
<div id="auth-verify-error" class="auth-error"></div>
|
|
<div id="auth-verify-success" class="auth-success"></div>
|
|
<div style="text-align: center; padding: 20px 0;">
|
|
<div id="auth-verify-loading" style="color: var(--bp-text-muted);">
|
|
E-Mail wird verifiziert...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification Preferences Modal -->
|
|
<div id="notification-prefs-modal" class="notification-prefs-modal">
|
|
<div class="notification-prefs-content">
|
|
<div class="auth-modal-header">
|
|
<h2>Benachrichtigungseinstellungen</h2>
|
|
<button class="legal-modal-close" onclick="closeNotificationPreferences()">×</button>
|
|
</div>
|
|
<div class="auth-body">
|
|
<div class="notification-pref-item">
|
|
<div>
|
|
<div class="notification-pref-label">E-Mail-Benachrichtigungen</div>
|
|
<div class="notification-pref-desc">Wichtige Updates per E-Mail erhalten</div>
|
|
</div>
|
|
<div class="toggle-switch active" id="pref-email-toggle" onclick="toggleNotificationPref('email')">
|
|
<div class="toggle-switch-handle"></div>
|
|
</div>
|
|
</div>
|
|
<div class="notification-pref-item">
|
|
<div>
|
|
<div class="notification-pref-label">In-App-Benachrichtigungen</div>
|
|
<div class="notification-pref-desc">Benachrichtigungen in der App anzeigen</div>
|
|
</div>
|
|
<div class="toggle-switch active" id="pref-inapp-toggle" onclick="toggleNotificationPref('inapp')">
|
|
<div class="toggle-switch-handle"></div>
|
|
</div>
|
|
</div>
|
|
<div class="notification-pref-item">
|
|
<div>
|
|
<div class="notification-pref-label">Push-Benachrichtigungen</div>
|
|
<div class="notification-pref-desc">Browser-Push-Benachrichtigungen aktivieren</div>
|
|
</div>
|
|
<div class="toggle-switch" id="pref-push-toggle" onclick="toggleNotificationPref('push')">
|
|
<div class="toggle-switch-handle"></div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top: 20px;">
|
|
<button class="auth-btn auth-btn-primary" onclick="saveNotificationPreferences()">Speichern</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
|
|
def get_auth_modal_js() -> str:
|
|
"""JavaScript für Auth Modal zurückgeben"""
|
|
return """
|
|
const authModal = document.getElementById('auth-modal');
|
|
const authModalClose = document.getElementById('auth-modal-close');
|
|
const authTabs = document.querySelectorAll('.auth-tab');
|
|
const authContents = document.querySelectorAll('.auth-content');
|
|
const btnLogin = document.getElementById('btn-login');
|
|
|
|
// Auth state
|
|
let currentUser = null;
|
|
let accessToken = localStorage.getItem('bp_access_token');
|
|
let refreshToken = localStorage.getItem('bp_refresh_token');
|
|
|
|
// Update UI based on auth state
|
|
function updateAuthUI() {
|
|
const loginBtn = document.getElementById('btn-login');
|
|
const userDropdown = document.querySelector('.auth-user-dropdown');
|
|
const notificationBell = document.getElementById('notification-bell');
|
|
|
|
if (currentUser && accessToken) {
|
|
// User is logged in - hide login button
|
|
if (loginBtn) loginBtn.style.display = 'none';
|
|
|
|
// Show notification bell
|
|
if (notificationBell) {
|
|
notificationBell.classList.add('active');
|
|
loadNotifications(); // Load notifications on login
|
|
startNotificationPolling(); // Start polling for new notifications
|
|
checkSuspensionStatus(); // Check if account is suspended
|
|
}
|
|
|
|
// Show user dropdown if it exists
|
|
if (userDropdown) {
|
|
userDropdown.classList.add('active');
|
|
const avatar = userDropdown.querySelector('.auth-user-avatar');
|
|
const menuName = userDropdown.querySelector('.auth-user-menu-name');
|
|
const menuEmail = userDropdown.querySelector('.auth-user-menu-email');
|
|
|
|
if (avatar) {
|
|
const initials = currentUser.name
|
|
? currentUser.name.substring(0, 2).toUpperCase()
|
|
: currentUser.email.substring(0, 2).toUpperCase();
|
|
avatar.textContent = initials;
|
|
}
|
|
if (menuName) menuName.textContent = currentUser.name || 'Benutzer';
|
|
if (menuEmail) menuEmail.textContent = currentUser.email;
|
|
}
|
|
} else {
|
|
// User is logged out - show login button
|
|
if (loginBtn) loginBtn.style.display = 'block';
|
|
if (userDropdown) userDropdown.classList.remove('active');
|
|
if (notificationBell) notificationBell.classList.remove('active');
|
|
stopNotificationPolling();
|
|
}
|
|
}
|
|
|
|
// Check if user is already logged in
|
|
async function checkAuthStatus() {
|
|
if (!accessToken) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/profile', {
|
|
headers: { 'Authorization': `Bearer ${accessToken}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
currentUser = await response.json();
|
|
updateAuthUI();
|
|
} else if (response.status === 401 && refreshToken) {
|
|
// Try to refresh the token
|
|
await refreshAccessToken();
|
|
} else {
|
|
// Clear invalid tokens
|
|
logout(false);
|
|
}
|
|
} catch (e) {
|
|
console.error('Auth check failed:', e);
|
|
}
|
|
}
|
|
|
|
// Refresh access token
|
|
async function refreshAccessToken() {
|
|
if (!refreshToken) return false;
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/refresh', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh_token: refreshToken })
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
accessToken = data.access_token;
|
|
refreshToken = data.refresh_token;
|
|
currentUser = data.user;
|
|
|
|
localStorage.setItem('bp_access_token', accessToken);
|
|
localStorage.setItem('bp_refresh_token', refreshToken);
|
|
updateAuthUI();
|
|
return true;
|
|
} else {
|
|
logout(false);
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
console.error('Token refresh failed:', e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Logout
|
|
function logout(showMessage = true) {
|
|
if (refreshToken) {
|
|
fetch('/api/auth/logout', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh_token: refreshToken })
|
|
}).catch(() => {});
|
|
}
|
|
|
|
currentUser = null;
|
|
accessToken = null;
|
|
refreshToken = null;
|
|
localStorage.removeItem('bp_access_token');
|
|
localStorage.removeItem('bp_refresh_token');
|
|
updateAuthUI();
|
|
|
|
if (showMessage) {
|
|
alert('Sie wurden erfolgreich abgemeldet.');
|
|
}
|
|
}
|
|
|
|
// Open auth modal
|
|
btnLogin?.addEventListener('click', () => {
|
|
authModal.classList.add('active');
|
|
showAuthTab('login');
|
|
clearAuthErrors();
|
|
});
|
|
|
|
// Close auth modal
|
|
authModalClose?.addEventListener('click', () => {
|
|
authModal.classList.remove('active');
|
|
clearAuthErrors();
|
|
});
|
|
|
|
// Close on background click
|
|
authModal?.addEventListener('click', (e) => {
|
|
if (e.target === authModal) {
|
|
authModal.classList.remove('active');
|
|
clearAuthErrors();
|
|
}
|
|
});
|
|
|
|
// Tab switching
|
|
authTabs.forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
const tabId = tab.dataset.tab;
|
|
showAuthTab(tabId);
|
|
});
|
|
});
|
|
|
|
function showAuthTab(tabId) {
|
|
authTabs.forEach(t => t.classList.remove('active'));
|
|
authContents.forEach(c => c.classList.remove('active'));
|
|
|
|
const activeTab = document.querySelector(`.auth-tab[data-tab="${tabId}"]`);
|
|
if (activeTab) activeTab.classList.add('active');
|
|
|
|
document.getElementById(`auth-${tabId}`)?.classList.add('active');
|
|
clearAuthErrors();
|
|
}
|
|
|
|
function clearAuthErrors() {
|
|
document.querySelectorAll('.auth-error, .auth-success').forEach(el => {
|
|
el.classList.remove('active');
|
|
el.textContent = '';
|
|
});
|
|
}
|
|
|
|
function showAuthError(elementId, message) {
|
|
const el = document.getElementById(elementId);
|
|
if (el) {
|
|
el.textContent = message;
|
|
el.classList.add('active');
|
|
}
|
|
}
|
|
|
|
function showAuthSuccess(elementId, message) {
|
|
const el = document.getElementById(elementId);
|
|
if (el) {
|
|
el.textContent = message;
|
|
el.classList.add('active');
|
|
}
|
|
}
|
|
|
|
// Login form
|
|
document.getElementById('auth-login-form')?.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
clearAuthErrors();
|
|
|
|
const email = document.getElementById('login-email').value;
|
|
const password = document.getElementById('login-password').value;
|
|
const btn = document.getElementById('login-btn');
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = 'Anmelden...';
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
accessToken = data.access_token;
|
|
refreshToken = data.refresh_token;
|
|
currentUser = data.user;
|
|
|
|
localStorage.setItem('bp_access_token', accessToken);
|
|
localStorage.setItem('bp_refresh_token', refreshToken);
|
|
|
|
updateAuthUI();
|
|
authModal.classList.remove('active');
|
|
|
|
// Clear form
|
|
document.getElementById('login-email').value = '';
|
|
document.getElementById('login-password').value = '';
|
|
} else {
|
|
showAuthError('auth-login-error', data.detail || data.error || 'Anmeldung fehlgeschlagen');
|
|
}
|
|
} catch (e) {
|
|
showAuthError('auth-login-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = 'Anmelden';
|
|
});
|
|
|
|
// Register form
|
|
document.getElementById('auth-register-form')?.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
clearAuthErrors();
|
|
|
|
const name = document.getElementById('register-name').value;
|
|
const email = document.getElementById('register-email').value;
|
|
const password = document.getElementById('register-password').value;
|
|
const passwordConfirm = document.getElementById('register-password-confirm').value;
|
|
|
|
if (password !== passwordConfirm) {
|
|
showAuthError('auth-register-error', 'Passwörter stimmen nicht überein');
|
|
return;
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
showAuthError('auth-register-error', 'Passwort muss mindestens 8 Zeichen lang sein');
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('register-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Registrieren...';
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password, name: name || undefined })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
showAuthSuccess('auth-register-success',
|
|
'Registrierung erfolgreich! Bitte überprüfen Sie Ihre E-Mails zur Bestätigung.');
|
|
|
|
// Clear form
|
|
document.getElementById('register-name').value = '';
|
|
document.getElementById('register-email').value = '';
|
|
document.getElementById('register-password').value = '';
|
|
document.getElementById('register-password-confirm').value = '';
|
|
} else {
|
|
showAuthError('auth-register-error', data.detail || data.error || 'Registrierung fehlgeschlagen');
|
|
}
|
|
} catch (e) {
|
|
showAuthError('auth-register-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = 'Registrieren';
|
|
});
|
|
|
|
// Forgot password form
|
|
document.getElementById('auth-forgot-form')?.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
clearAuthErrors();
|
|
|
|
const email = document.getElementById('forgot-email').value;
|
|
const btn = document.getElementById('forgot-btn');
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = 'Senden...';
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/forgot-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email })
|
|
});
|
|
|
|
// Always show success to prevent email enumeration
|
|
showAuthSuccess('auth-forgot-success',
|
|
'Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.');
|
|
|
|
document.getElementById('forgot-email').value = '';
|
|
} catch (e) {
|
|
showAuthError('auth-forgot-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = 'Link senden';
|
|
});
|
|
|
|
// Reset password form
|
|
document.getElementById('auth-reset-form')?.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
clearAuthErrors();
|
|
|
|
const password = document.getElementById('reset-password').value;
|
|
const passwordConfirm = document.getElementById('reset-password-confirm').value;
|
|
const token = document.getElementById('reset-token').value;
|
|
|
|
if (password !== passwordConfirm) {
|
|
showAuthError('auth-reset-error', 'Passwörter stimmen nicht überein');
|
|
return;
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
showAuthError('auth-reset-error', 'Passwort muss mindestens 8 Zeichen lang sein');
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('reset-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Ändern...';
|
|
|
|
try {
|
|
const response = await fetch('/api/auth/reset-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token, new_password: password })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
showAuthSuccess('auth-reset-success',
|
|
'Passwort erfolgreich geändert! Sie können sich jetzt anmelden.');
|
|
|
|
// Clear URL params
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
|
|
// Switch to login after 2 seconds
|
|
setTimeout(() => showAuthTab('login'), 2000);
|
|
} else {
|
|
showAuthError('auth-reset-error', data.detail || data.error || 'Passwort zurücksetzen fehlgeschlagen');
|
|
}
|
|
} catch (e) {
|
|
showAuthError('auth-reset-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = 'Passwort ändern';
|
|
});
|
|
|
|
// Navigation links
|
|
document.getElementById('auth-forgot-password')?.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
showAuthTab('forgot');
|
|
// Hide tabs for forgot password
|
|
document.querySelector('.auth-tabs').style.display = 'none';
|
|
});
|
|
|
|
document.getElementById('auth-back-to-login')?.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
showAuthTab('login');
|
|
document.querySelector('.auth-tabs').style.display = 'flex';
|
|
});
|
|
|
|
document.getElementById('auth-goto-login')?.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
showAuthTab('login');
|
|
});
|
|
|
|
// Check for URL parameters (email verification, password reset)
|
|
function checkAuthUrlParams() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const verifyToken = urlParams.get('verify');
|
|
const resetToken = urlParams.get('reset');
|
|
|
|
if (verifyToken) {
|
|
authModal.classList.add('active');
|
|
document.querySelector('.auth-tabs').style.display = 'none';
|
|
showAuthTab('verify');
|
|
verifyEmail(verifyToken);
|
|
} else if (resetToken) {
|
|
authModal.classList.add('active');
|
|
document.querySelector('.auth-tabs').style.display = 'none';
|
|
showAuthTab('reset');
|
|
document.getElementById('reset-token').value = resetToken;
|
|
}
|
|
}
|
|
|
|
async function verifyEmail(token) {
|
|
try {
|
|
const response = await fetch('/api/auth/verify-email', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token })
|
|
});
|
|
|
|
const data = await response.json();
|
|
const loadingEl = document.getElementById('auth-verify-loading');
|
|
|
|
if (response.ok) {
|
|
if (loadingEl) loadingEl.style.display = 'none';
|
|
showAuthSuccess('auth-verify-success', 'E-Mail erfolgreich verifiziert! Sie können sich jetzt anmelden.');
|
|
|
|
// Clear URL params
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
|
|
// Switch to login after 2 seconds
|
|
setTimeout(() => {
|
|
showAuthTab('login');
|
|
document.querySelector('.auth-tabs').style.display = 'flex';
|
|
}, 2000);
|
|
} else {
|
|
if (loadingEl) loadingEl.style.display = 'none';
|
|
showAuthError('auth-verify-error', data.detail || data.error || 'Verifizierung fehlgeschlagen. Der Link ist möglicherweise abgelaufen.');
|
|
}
|
|
} catch (e) {
|
|
document.getElementById('auth-verify-loading').style.display = 'none';
|
|
showAuthError('auth-verify-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
|
}
|
|
}
|
|
|
|
// User dropdown toggle
|
|
const authUserBtn = document.getElementById('auth-user-btn');
|
|
const authUserMenu = document.getElementById('auth-user-menu');
|
|
|
|
authUserBtn?.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
authUserMenu.classList.toggle('active');
|
|
});
|
|
|
|
// Close dropdown when clicking outside
|
|
document.addEventListener('click', () => {
|
|
authUserMenu?.classList.remove('active');
|
|
});
|
|
|
|
// Placeholder functions for profile/sessions
|
|
function showProfileModal() {
|
|
alert('Profil-Einstellungen kommen bald!');
|
|
}
|
|
|
|
function showSessionsModal() {
|
|
alert('Sitzungsverwaltung kommt bald!');
|
|
}
|
|
"""
|