add auth via uuid only

This commit is contained in:
2026-01-31 15:25:33 +01:00
parent 57020a8913
commit f919079f69
9 changed files with 113 additions and 22 deletions

View File

@@ -261,6 +261,17 @@ app-header .btn-create-text {
display: none; display: none;
} }
app-header .btn-login,
app-header .btn-profile {
padding: var(--space-sm);
border-radius: var(--radius-full);
}
app-header .btn-login-text,
app-header .btn-profile-text {
display: none;
}
@media (min-width: 480px) { @media (min-width: 480px) {
app-header .btn-create { app-header .btn-create {
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) var(--space-md);
@@ -270,6 +281,17 @@ app-header .btn-create-text {
app-header .btn-create-text { app-header .btn-create-text {
display: inline; display: inline;
} }
app-header .btn-login,
app-header .btn-profile {
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-md);
}
app-header .btn-login-text,
app-header .btn-profile-text {
display: inline;
}
} }
/* Footer Component */ /* Footer Component */

View File

@@ -1,5 +1,6 @@
import { i18n, t } from '../i18n.js'; import { i18n, t } from '../i18n.js';
import { router } from '../router.js'; import { router } from '../router.js';
import { auth } from '../services/auth.js';
class AppHeader extends HTMLElement { class AppHeader extends HTMLElement {
constructor() { constructor() {
@@ -14,11 +15,18 @@ class AppHeader extends HTMLElement {
this.setupEventListeners(); this.setupEventListeners();
document.addEventListener('click', this.handleOutsideClick); document.addEventListener('click', this.handleOutsideClick);
document.addEventListener('keydown', this.handleKeydown); document.addEventListener('keydown', this.handleKeydown);
// Subscribe to auth changes (only once!)
this.authUnsubscribe = auth.subscribe(() => {
this.render();
this.setupEventListeners();
});
} }
disconnectedCallback() { disconnectedCallback() {
document.removeEventListener('click', this.handleOutsideClick); document.removeEventListener('click', this.handleOutsideClick);
document.removeEventListener('keydown', this.handleKeydown); document.removeEventListener('keydown', this.handleKeydown);
if (this.authUnsubscribe) this.authUnsubscribe();
} }
handleOutsideClick() { handleOutsideClick() {
@@ -110,15 +118,15 @@ class AppHeader extends HTMLElement {
<div class="dropdown" id="lang-dropdown"> <div class="dropdown" id="lang-dropdown">
<button <button
class="btn btn-outline" class="btn btn-icon btn-outline"
id="lang-toggle" id="lang-toggle"
aria-haspopup="listbox" aria-haspopup="listbox"
aria-expanded="${this.langDropdownOpen}" aria-expanded="${this.langDropdownOpen}"
aria-label="${t('header.selectLanguage')}" aria-label="${t('header.selectLanguage')}"
title="${t('header.selectLanguage')}"
> >
<span id="current-lang">${i18n.getLocale().toUpperCase()}</span> <svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M8 .25a7.77 7.77 0 0 1 7.75 7.78 7.75 7.75 0 0 1-7.52 7.72h-.25A7.75 7.75 0 0 1 .25 8.24v-.25A7.75 7.75 0 0 1 8 .25zm1.95 8.5h-3.9c.15 2.9 1.17 5.34 1.88 5.5H8c.68 0 1.72-2.37 1.93-5.23zm4.26 0h-2.76c-.09 1.96-.53 3.78-1.18 5.08A6.26 6.26 0 0 0 14.17 9zm-9.67 0H1.8a6.26 6.26 0 0 0 3.94 5.08 12.59 12.59 0 0 1-1.16-4.7l-.03-.38zm1.2-6.58-.12.05a6.26 6.26 0 0 0-3.83 5.03h2.75c.09-1.83.48-3.54 1.06-4.81zm2.25-.42c-.7 0-1.78 2.51-1.94 5.5h3.9c-.15-2.9-1.18-5.34-1.89-5.5h-.07zm2.28.43.03.05a12.95 12.95 0 0 1 1.15 5.02h2.75a6.28 6.28 0 0 0-3.93-5.07z"></path>
<polyline points="6 9 12 15 18 9"></polyline>
</svg> </svg>
</button> </button>
<div class="dropdown-menu" role="listbox" aria-label="${t('header.selectLanguage')}"> <div class="dropdown-menu" role="listbox" aria-label="${t('header.selectLanguage')}">
@@ -134,6 +142,24 @@ class AppHeader extends HTMLElement {
`).join('')} `).join('')}
</div> </div>
</div> </div>
${auth.isLoggedIn() ? `
<button class="btn btn-outline btn-profile" id="profile-btn" title="${t('header.profile')}">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<span class="btn-profile-text">${t('header.profile')}</span>
</button>
` : `
<button class="btn btn-outline btn-login" id="login-btn" title="${t('auth.login')}">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<span class="btn-login-text">${t('auth.login')}</span>
</button>
`}
</div> </div>
</div> </div>
`; `;
@@ -164,6 +190,21 @@ class AppHeader extends HTMLElement {
}); });
}); });
// Login button
const loginBtn = this.querySelector('#login-btn');
loginBtn?.addEventListener('click', () => {
const authModal = document.querySelector('auth-modal');
if (authModal) {
authModal.show('login');
}
});
// Profile button
const profileBtn = this.querySelector('#profile-btn');
profileBtn?.addEventListener('click', () => {
router.navigate('/profile');
});
this.updateThemeIcon(); this.updateThemeIcon();
} }

View File

@@ -1,7 +1,9 @@
import { router } from '../router.js'; import { router } from '../router.js';
import { i18n } from '../i18n.js'; import { i18n } from '../i18n.js';
import { auth } from '../services/auth.js';
import './app-header.js'; import './app-header.js';
import './app-footer.js'; import './app-footer.js';
import './auth-modal.js';
import './pages/page-home.js'; import './pages/page-home.js';
import './pages/page-search.js'; import './pages/page-search.js';
import './pages/page-listing.js'; import './pages/page-listing.js';
@@ -29,9 +31,13 @@ class AppShell extends HTMLElement {
<app-header></app-header> <app-header></app-header>
<main class="container" id="router-outlet"></main> <main class="container" id="router-outlet"></main>
<app-footer></app-footer> <app-footer></app-footer>
<auth-modal hidden></auth-modal>
`; `;
this.main = this.querySelector('#router-outlet'); this.main = this.querySelector('#router-outlet');
// Try to restore session
auth.tryRestoreSession();
} }
setupRouter() { setupRouter() {

View File

@@ -20,7 +20,17 @@ class AuthService {
* @returns {string} UUID v4 * @returns {string} UUID v4
*/ */
generateUuid() { generateUuid() {
return crypto.randomUUID(); // Use native if available (secure contexts)
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
// Fallback for non-secure contexts
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
} }
/** /**
@@ -43,8 +53,18 @@ class AuthService {
try { try {
await directus.register(email, uuid); await directus.register(email, uuid);
// Auto-login after registration // Try auto-login (may fail if verification required)
await this.login(uuid); const loginResult = await this.login(uuid);
if (!loginResult.success) {
// Registration worked but login failed (verification pending)
return {
uuid,
success: true,
pendingVerification: true,
message: 'Account created. Login may require activation.'
};
}
return { uuid, success: true }; return { uuid, success: true };
} catch (error) { } catch (error) {
@@ -69,8 +89,6 @@ class AuthService {
await directus.login(email, uuid); await directus.login(email, uuid);
this.currentUser = await directus.getCurrentUser(); this.currentUser = await directus.getCurrentUser();
this.notifyListeners(); this.notifyListeners();
// Store UUID for convenience (optional)
this.storeUuid(uuid); this.storeUuid(uuid);
return { success: true }; return { success: true };

View File

@@ -100,12 +100,15 @@ class DirectusService {
headers headers
}); });
// Token abgelaufen - versuche Refresh // Token abgelaufen - versuche Refresh (aber nicht für auth-Endpoints)
if (response.status === 401 && this.refreshToken) { if (response.status === 401 && this.refreshToken && !endpoint.startsWith('/auth/')) {
const refreshed = await this.refreshSession(); const refreshed = await this.refreshSession();
if (refreshed) { if (refreshed) {
headers['Authorization'] = `Bearer ${this.accessToken}`; headers['Authorization'] = `Bearer ${this.accessToken}`;
return this.request(endpoint, options); return this.request(endpoint, options);
} else {
// Refresh failed - clear tokens to prevent loops
this.clearTokens();
} }
} }
@@ -216,13 +219,9 @@ class DirectusService {
return false; return false;
} }
async register(email, password, userData = {}) { async register(email, password) {
return this.post('/users', { // Public registration (no verification required)
email, return this.post('/users/register', { email, password });
password,
...userData,
role: null // Wird durch Directus Flow/Policy gesetzt
});
} }
async requestPasswordReset(email) { async requestPasswordReset(email) {

View File

@@ -3,7 +3,8 @@
"searchPlaceholder": "Was suchst du?", "searchPlaceholder": "Was suchst du?",
"createListing": "Anzeige erstellen", "createListing": "Anzeige erstellen",
"toggleTheme": "Design wechseln", "toggleTheme": "Design wechseln",
"selectLanguage": "Sprache auswählen" "selectLanguage": "Sprache auswählen",
"profile": "Profil"
}, },
"footer": { "footer": {
"rights": "Alle Rechte vorbehalten.", "rights": "Alle Rechte vorbehalten.",

View File

@@ -3,7 +3,8 @@
"searchPlaceholder": "What are you looking for?", "searchPlaceholder": "What are you looking for?",
"createListing": "Create Listing", "createListing": "Create Listing",
"toggleTheme": "Toggle theme", "toggleTheme": "Toggle theme",
"selectLanguage": "Select language" "selectLanguage": "Select language",
"profile": "Profile"
}, },
"footer": { "footer": {
"rights": "All rights reserved.", "rights": "All rights reserved.",

View File

@@ -3,7 +3,8 @@
"searchPlaceholder": "Que cherchez-vous ?", "searchPlaceholder": "Que cherchez-vous ?",
"createListing": "Créer une annonce", "createListing": "Créer une annonce",
"toggleTheme": "Changer de thème", "toggleTheme": "Changer de thème",
"selectLanguage": "Choisir la langue" "selectLanguage": "Choisir la langue",
"profile": "Profil"
}, },
"footer": { "footer": {
"rights": "Tous droits réservés.", "rights": "Tous droits réservés.",

View File

@@ -1,4 +1,4 @@
const CACHE_NAME = 'dgray-v20'; const CACHE_NAME = 'dgray-v26';
const STATIC_ASSETS = [ const STATIC_ASSETS = [
'/', '/',
'/index.html', '/index.html',
@@ -12,6 +12,8 @@ const STATIC_ASSETS = [
'/js/services/api.js', '/js/services/api.js',
'/js/services/crypto.js', '/js/services/crypto.js',
'/js/services/chat.js', '/js/services/chat.js',
'/js/services/directus.js',
'/js/services/auth.js',
'/js/data/mock-listings.js', '/js/data/mock-listings.js',
'/js/components/chat-widget.js', '/js/components/chat-widget.js',
'/js/components/listing-card.js', '/js/components/listing-card.js',