add auth via uuid only
This commit is contained in:
@@ -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 */
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user