Fix Appwrite-Session und Collection-IDs für Ticket-System

Nutzt String-Collection-IDs, ticket.webklar.com/v1 als Default und
korrigiert isAdmin sowie Session-Handling für Same-Origin-Login.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Webklar Deploy
2026-05-22 17:58:11 +00:00
parent 5fbb2fb4b5
commit 4f4de7f290
3 changed files with 109 additions and 62 deletions

9
package-lock.json generated
View File

@@ -56,7 +56,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1097,7 +1096,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -1174,7 +1172,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -1785,7 +1782,6 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -1797,7 +1793,6 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -1953,8 +1948,7 @@
"version": "0.182.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tr46": {
"version": "0.0.3",
@@ -2002,7 +1996,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",

View File

@@ -1,24 +1,61 @@
import { createContext, useContext, useState, useEffect } from 'react'
import { account, databases, DATABASE_ID, COLLECTIONS, ID, Query } from '../lib/appwrite'
import { account, databases, DATABASE_ID, COLLECTIONS, ID, Query, hasAppwriteSession } from '../lib/appwrite'
const AuthContext = createContext()
// Demo mode when Appwrite is not configured
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
const DEMO_MODE = !(import.meta.env.VITE_APPWRITE_PROJECT_ID || '').trim()
const PROJECT_ID = (import.meta.env.VITE_APPWRITE_PROJECT_ID || '').trim()
function clearStaleAppwriteSessions() {
if (typeof window === 'undefined' || !window.localStorage) return
try {
const raw = window.localStorage.getItem('cookieFallback')
if (!raw) return
const parsed = JSON.parse(raw)
if (typeof parsed !== 'object' || parsed === null) return
for (const key of Object.keys(parsed)) {
if (key.startsWith('a_session_') && key !== `a_session_${PROJECT_ID}`) {
delete parsed[key]
}
}
window.localStorage.setItem('cookieFallback', JSON.stringify(parsed))
} catch {
window.localStorage.removeItem('cookieFallback')
}
}
function clearAllLocalAppwriteSessions() {
if (typeof window === 'undefined') return
try {
window.localStorage.removeItem('cookieFallback')
} catch { /* ignore */ }
}
async function createSessionWithCleanup(email, password) {
try {
await account.deleteSessions()
} catch { /* alte Sitzung ggf. nur serverseitig */ }
clearAllLocalAppwriteSessions()
if (account.createEmailPasswordSession) {
await account.createEmailPasswordSession(email, password)
} else {
await account.createEmailSession(email, password)
}
}
// Hilfsfunktion: Fügt User automatisch zur employees Collection hinzu
async function ensureEmployeeExists(user) {
if (!user || DEMO_MODE) return
try {
// Prüfe ob User bereits in employees Collection existiert
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
[Query.equal('userId', user.$id)]
)
// Wenn User noch nicht existiert, füge ihn hinzu
if (response.documents.length === 0) {
await databases.createDocument(
DATABASE_ID,
@@ -28,13 +65,12 @@ async function ensureEmployeeExists(user) {
userId: user.$id,
displayName: user.name || user.email,
email: user.email,
shortcode: '' // Kürzel wird später vom Admin hinzugefügt
shortcode: ''
}
)
console.log('✅ User automatisch zur Mitarbeiter-Liste hinzugefügt')
}
} catch (error) {
// Fehler ignorieren wenn Collection nicht existiert oder Permissions fehlen
if (error.code !== 404) {
console.warn('Could not add user to employees collection:', error.message)
}
@@ -51,7 +87,6 @@ export function AuthProvider({ children }) {
async function checkUser() {
if (DEMO_MODE) {
// Check localStorage for demo session
const demoUser = localStorage.getItem('demo_user')
if (demoUser) {
setUser(JSON.parse(demoUser))
@@ -60,17 +95,23 @@ export function AuthProvider({ children }) {
return
}
if (!hasAppwriteSession()) {
setUser(null)
setLoading(false)
return
}
try {
const session = await account.get()
setUser(session)
// Automatisch zur employees Collection hinzufügen
await ensureEmployeeExists(session)
} catch (error) {
// Kein Fehler loggen beim initialen Check - das ist normal wenn nicht eingeloggt
// Nur loggen wenn es ein unerwarteter Fehler ist (nicht 401)
if (error.code !== 401 && error.code !== 404) {
console.error('Unexpected error checking user:', error)
}
if (error.code === 401) {
clearAllLocalAppwriteSessions()
}
setUser(null)
} finally {
setLoading(false)
@@ -79,7 +120,6 @@ export function AuthProvider({ children }) {
async function login(email, password) {
if (DEMO_MODE) {
// Demo login - accept any credentials
const demoUser = { $id: 'demo', email, name: email.split('@')[0] }
localStorage.setItem('demo_user', JSON.stringify(demoUser))
setUser(demoUser)
@@ -87,19 +127,12 @@ export function AuthProvider({ children }) {
}
try {
// Appwrite 1.5.7 / SDK 13.0 - versuche beide Methoden für Kompatibilität
try {
await account.createEmailSession(email, password)
} catch (e) {
// Fallback für ältere API
if (account.createEmailPasswordSession) {
await account.createEmailPasswordSession(email, password)
} else {
throw e
}
}
const normalizedEmail = email.trim()
const normalizedPassword = password.trim()
clearStaleAppwriteSessions()
await createSessionWithCleanup(normalizedEmail, normalizedPassword)
// User-Daten laden und automatisch zur employees Collection hinzufügen
const session = await account.get()
setUser(session)
await ensureEmployeeExists(session)
@@ -109,8 +142,16 @@ export function AuthProvider({ children }) {
console.error('Login error:', error)
let errorMessage = error.message || 'Login fehlgeschlagen'
// Bessere Fehlermeldungen
if (error.code === 401 || errorMessage.includes('Invalid credentials')) {
if (error.code === 429 || errorMessage.includes('Rate limit')) {
errorMessage = 'Zu viele Login-Versuche. Bitte 1530 Minuten warten und es erneut versuchen.'
} else if (errorMessage.includes('missing scopes') && errorMessage.includes('account')) {
errorMessage = 'Session konnte nicht gespeichert werden. Bitte Seite neu laden und erneut versuchen.'
} else if (
error?.type === 'user_session_already_exists' ||
errorMessage.includes('session is active')
) {
errorMessage = 'Es gibt noch eine alte Sitzung. Bitte Seite neu laden und erneut anmelden.'
} else if (error.code === 401 || errorMessage.includes('Invalid credentials')) {
errorMessage = 'Ungültige Email oder Passwort'
} else if (errorMessage.includes('User not found')) {
errorMessage = 'Benutzer nicht gefunden. Bitte registriere dich zuerst.'
@@ -130,26 +171,23 @@ export function AuthProvider({ children }) {
}
try {
await account.deleteSession('current')
setUser(null)
await account.deleteSessions()
} catch (error) {
console.error('Logout error:', error)
} finally {
clearAllLocalAppwriteSessions()
setUser(null)
}
}
// Hilfsfunktion um zu prüfen ob Benutzer Admin ist
const isAdmin = () => {
if (!user) return false
// Prüfe ob Benutzer das "admin" Label hat
return user.labels?.includes('admin') || false
}
const isAdmin = Boolean(user?.labels?.includes('admin'))
const value = {
user,
loading,
login,
logout,
isAdmin: isAdmin()
isAdmin
}
return (

View File

@@ -1,6 +1,5 @@
import { Client, Account, Databases, Storage, ID, Query } from 'appwrite'
// Debug: Zeige geladene Umgebungsvariablen (nur in Development)
if (import.meta.env.DEV) {
console.log('🔧 Appwrite Konfiguration:')
console.log('Endpoint:', import.meta.env.VITE_APPWRITE_ENDPOINT || 'NICHT GESETZT')
@@ -8,8 +7,8 @@ if (import.meta.env.DEV) {
console.log('Database ID:', import.meta.env.VITE_APPWRITE_DATABASE_ID || 'NICHT GESETZT')
}
const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT || 'https://appwrite.webklar.com/v1'
const projectId = import.meta.env.VITE_APPWRITE_PROJECT_ID || ''
const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT || 'https://ticket.webklar.com/v1'
const projectId = (import.meta.env.VITE_APPWRITE_PROJECT_ID || '').trim()
if (!projectId) {
console.error('❌ FEHLER: VITE_APPWRITE_PROJECT_ID ist nicht gesetzt!')
@@ -26,18 +25,35 @@ export const storage = new Storage(client)
export const DATABASE_ID = import.meta.env.VITE_APPWRITE_DATABASE_ID || 'woms-database'
// Collection IDs - Verwende die tatsächlichen Collection IDs aus Appwrite!
export const COLLECTIONS = {
WORKORDERS: '6943bf7d001901baa60c', // Collection ID für workorders
CONFIG: 'config', // Collection ID für Admin-Konfiguration (wird erstellt)
CUSTOMERS: '694bd1fb002b2e583d13', // Collection ID für customers
EMPLOYEES: '695280510031c6c6153b', // Collection ID für employees
WORKSHEETS: '6952dbcf0032a92e1168', // Collection ID für worksheets
WORKORDERS: 'workorders',
CONFIG: 'config',
CUSTOMERS: 'customers',
EMPLOYEES: 'employees',
WORKSHEETS: 'worksheets',
USERS: 'users',
ATTACHMENTS: 'attachments'
}
export const BUCKET_ID = 'woms-attachments'
export const BUCKET_ID = import.meta.env.VITE_APPWRITE_BUCKET_ID || 'woms-attachments'
/** Prüft ob vermutlich eine Session existiert (cookieFallback oder Same-Origin-Cookie). */
export function hasAppwriteSession() {
if (typeof window === 'undefined' || !projectId) return false
try {
const apiHost = new URL(endpoint).hostname
if (apiHost === window.location.hostname) {
return true
}
} catch { /* ignore */ }
try {
const raw = window.localStorage.getItem('cookieFallback')
if (!raw) return false
const parsed = JSON.parse(raw)
return Boolean(parsed?.[`a_session_${projectId}`])
} catch {
return false
}
}
// Helper functions
export { ID, Query }