diff --git a/scripts/setup-legal.mjs b/scripts/setup-legal.mjs index 5bd6c05..b4ff86a 100644 --- a/scripts/setup-legal.mjs +++ b/scripts/setup-legal.mjs @@ -11,6 +11,7 @@ */ import { pathToFileURL } from "node:url"; +import { staffPermissions } from "../src/lib/staffAccess.js"; const dryRun = process.argv.includes("--dry-run"); const endpoint = process.env.APPWRITE_ENDPOINT || "https://appwrite.webklar.com/v1"; @@ -18,8 +19,9 @@ const projectId = process.env.APPWRITE_PROJECT_ID || "6a1058610003c5a13a05"; const databaseId = process.env.APPWRITE_DATABASE_ID || "woms-database"; const apiKey = process.env.APPWRITE_API_KEY; -const READ_WRITE = ['read("users")', 'create("users")', 'update("users")']; -const CRUD = [...READ_WRITE, 'delete("users")']; +// Nur Mitarbeiter (Labels), nie "users": das waeren auch alle Kunden-Konten des Portals +const READ_WRITE = staffPermissions("read", "create", "update"); +const CRUD = staffPermissions("read", "create", "update", "delete"); const date = (key) => ({ key, type: "string", size: 10 }); @@ -81,7 +83,7 @@ export const COLLECTIONS = [ export const BUCKET = { id: "legal-files", name: "Rechtliches Dateien", - perms: ['read("users")', 'create("users")'], + perms: staffPermissions("read", "create"), maxSize: 20 * 1024 * 1024, extensions: ["pdf", "html", "htm", "png", "jpg", "jpeg", "webp", "doc", "docx"], }; diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx index 3a13f27..e3a16fd 100644 --- a/src/context/AuthContext.jsx +++ b/src/context/AuthContext.jsx @@ -11,6 +11,7 @@ import { client, } from '../lib/appwrite' import { createMfaApi, isMoreFactorsError, maskEmail, mfaErrorMessage } from '../lib/mfa' +import { isStaffAccount, NO_STAFF_ACCESS } from '../lib/staffAccess' const AuthContext = createContext() const mfa = createMfaApi(client) @@ -52,8 +53,16 @@ async function createSessionWithCleanup(email, password) { } } -// Hilfsfunktion: Fügt User automatisch zur employees Collection hinzu und liefert -// den Mitarbeiter-Eintrag (Kürzel = Gitea-Name, siehe isWorksheetCreator) oder null +// Konto ohne Mitarbeiter-Label (z. B. Kunde mit Portal-Passwort): Sitzung sofort beenden +async function endSession() { + try { + await account.deleteSession('current') + } catch { /* Sitzung ggf. schon weg */ } + clearAllLocalAppwriteSessions() +} + +// Hilfsfunktion: Fügt einen Mitarbeiter (nur nach isStaffAccount) automatisch zur employees Collection hinzu und +// liefert den Mitarbeiter-Eintrag (Kürzel = Gitea-Name, siehe isWorksheetCreator) oder null async function ensureEmployeeExists(user) { if (!user) return null @@ -109,6 +118,11 @@ export function AuthProvider({ children }) { try { const session = await account.get() + if (!isStaffAccount(session)) { + await endSession() + setUser(null) + return + } setUser(session) setEmployee(await ensureEmployeeExists(session)) } catch (error) { @@ -147,6 +161,10 @@ export function AuthProvider({ children }) { const sent = await sendCode() return { success: false, mfaRequired: true, error: sent.error } } + if (!isStaffAccount(session)) { + await endSession() + return { success: false, error: NO_STAFF_ACCESS } + } setUser(session) setEmployee(await ensureEmployeeExists(session)) @@ -191,6 +209,11 @@ export function AuthProvider({ children }) { try { await mfa.completeChallenge(mfaPending.challengeId, String(otp || '').trim()) const session = await account.get() + if (!isStaffAccount(session)) { + await endSession() + setMfaPending(null) + return { success: false, error: NO_STAFF_ACCESS } + } setUser(session) setEmployee(await ensureEmployeeExists(session)) setMfaPending(null) diff --git a/src/lib/staffAccess.js b/src/lib/staffAccess.js new file mode 100644 index 0000000..7839ee6 --- /dev/null +++ b/src/lib/staffAccess.js @@ -0,0 +1,21 @@ +/** + * Zugang zum Ticketsystem nur fuer Mitarbeiter: Appwrite-Label "mitarbeiter" oder "admin". Kunden haben Konten + * im selben Appwrite-Projekt (Kundenportal) und konnten sich mit ihrem Portal-Passwort hier anmelden. Labels + * setzt nur der Server-Key; dieselben Labels tragen die Rechte der Collections und Buckets + * (Kundenportal scripts/mitarbeiter-zugang.mjs) und der Team-Eingang des Portals. + * Bewusst ohne Import von appwrite.js, damit Node dieses Modul in den Tests laden kann. + */ +export const STAFF_LABELS = ['mitarbeiter', 'admin'] + +export const NO_STAFF_ACCESS = + 'Dieses Konto hat keinen Zugang zum Ticketsystem. Kunden melden sich unter project.webklar.com an.' + +export function isStaffAccount(account) { + const labels = Array.isArray(account?.labels) ? account.labels : [] + return STAFF_LABELS.some((label) => labels.includes(label)) +} + +/** Appwrite-Rechte nur fuer Mitarbeiter, z. B. staffPermissions('read', 'create') */ +export function staffPermissions(...actions) { + return actions.flatMap((action) => STAFF_LABELS.map((label) => `${action}("label:${label}")`)) +} diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 0aaf3a5..aff47a2 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -3,6 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom' import { useAuth } from '../context/AuthContext' import LetterGlitch from '../components/LetterGlitch' import { safeReturnPath } from '../lib/returnPath' +import { NO_STAFF_ACCESS } from '../lib/staffAccess' export default function LoginPage() { const [email, setEmail] = useState('') @@ -33,7 +34,9 @@ export default function LoginPage() { // Bessere Fehlermeldungen let errorMessage = result.error || 'Login fehlgeschlagen' - if (errorMessage.includes('Invalid credentials') || errorMessage.includes('401')) { + if (errorMessage === NO_STAFF_ACCESS) { + // Kunden-Konto: Hinweis unveraendert anzeigen + } else if (errorMessage.includes('Invalid credentials') || errorMessage.includes('401')) { errorMessage = 'Ungültige Email oder Passwort. Bitte überprüfe deine Eingaben.' } else if (errorMessage.includes('Email/Password') || errorMessage.includes('auth')) { errorMessage = 'Email/Password Authentifizierung ist möglicherweise nicht aktiviert. Bitte überprüfe deine Appwrite-Konfiguration.' diff --git a/tests/staffAccess.test.mjs b/tests/staffAccess.test.mjs new file mode 100644 index 0000000..d80d42c --- /dev/null +++ b/tests/staffAccess.test.mjs @@ -0,0 +1,32 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { isStaffAccount, staffPermissions, NO_STAFF_ACCESS } from '../src/lib/staffAccess.js' +import { COLLECTIONS, BUCKET } from '../scripts/setup-legal.mjs' + +test('isStaffAccount: nur Label mitarbeiter oder admin', () => { + assert.equal(isStaffAccount({ labels: ['mitarbeiter'] }), true) + assert.equal(isStaffAccount({ labels: ['admin'] }), true) + assert.equal(isStaffAccount({ labels: [] }), false) + assert.equal(isStaffAccount({}), false) + assert.equal(isStaffAccount(null), false) +}) + +test('staffPermissions: jede Aktion fuer beide Labels', () => { + assert.deepEqual(staffPermissions('read', 'create'), [ + 'read("label:mitarbeiter")', + 'read("label:admin")', + 'create("label:mitarbeiter")', + 'create("label:admin")', + ]) +}) + +test('Hinweis nennt das Kundenportal', () => { + assert.match(NO_STAFF_ACCESS, /project\.webklar\.com/) +}) + +test('setup-legal: keine Rechte fuer users/any/guests mehr (ein erneuter Lauf darf nichts oeffnen)', () => { + for (const perms of [...COLLECTIONS.map((c) => c.perms), BUCKET.perms]) { + assert.ok(perms.length > 0) + for (const p of perms) assert.match(p, /\("label:(mitarbeiter|admin)"\)$/, p) + } +})