Login nur fuer Mitarbeiter (Label), Kunden-Konten werden abgewiesen
Kunden haben Konten im selben Appwrite-Projekt und konnten sich mit ihrem Portal-Passwort im Ticketsystem anmelden; dabei entstand sogar automatisch ein employees-Eintrag. - lib/staffAccess.js: Zugang nur mit Label "mitarbeiter" oder "admin" - AuthContext: Konto ohne Label -> Sitzung sofort beenden, Hinweis aufs Kundenportal (Login, Neuladen, nach dem 2FA-Code); employees-Eintrag nur noch fuer Mitarbeiter - setup-legal.mjs: Rechte nur fuer die Labels (ein erneuter Lauf oeffnet nichts mehr fuer "users") Die eigentliche Sperre sind die Appwrite-Rechte (Kundenportal scripts/mitarbeiter-zugang.mjs --rechte). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
65e9931f2c
commit
78bc99a73b
5 files changed
+87
-6
No files matched your search
@@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
|
import { staffPermissions } from "../src/lib/staffAccess.js";
|
||||||
|
|
||||||
const dryRun = process.argv.includes("--dry-run");
|
const dryRun = process.argv.includes("--dry-run");
|
||||||
const endpoint = process.env.APPWRITE_ENDPOINT || "https://appwrite.webklar.com/v1";
|
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 databaseId = process.env.APPWRITE_DATABASE_ID || "woms-database";
|
||||||
const apiKey = process.env.APPWRITE_API_KEY;
|
const apiKey = process.env.APPWRITE_API_KEY;
|
||||||
|
|
||||||
const READ_WRITE = ['read("users")', 'create("users")', 'update("users")'];
|
// Nur Mitarbeiter (Labels), nie "users": das waeren auch alle Kunden-Konten des Portals
|
||||||
const CRUD = [...READ_WRITE, 'delete("users")'];
|
const READ_WRITE = staffPermissions("read", "create", "update");
|
||||||
|
const CRUD = staffPermissions("read", "create", "update", "delete");
|
||||||
|
|
||||||
const date = (key) => ({ key, type: "string", size: 10 });
|
const date = (key) => ({ key, type: "string", size: 10 });
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ export const COLLECTIONS = [
|
|||||||
export const BUCKET = {
|
export const BUCKET = {
|
||||||
id: "legal-files",
|
id: "legal-files",
|
||||||
name: "Rechtliches Dateien",
|
name: "Rechtliches Dateien",
|
||||||
perms: ['read("users")', 'create("users")'],
|
perms: staffPermissions("read", "create"),
|
||||||
maxSize: 20 * 1024 * 1024,
|
maxSize: 20 * 1024 * 1024,
|
||||||
extensions: ["pdf", "html", "htm", "png", "jpg", "jpeg", "webp", "doc", "docx"],
|
extensions: ["pdf", "html", "htm", "png", "jpg", "jpeg", "webp", "doc", "docx"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
client,
|
client,
|
||||||
} from '../lib/appwrite'
|
} from '../lib/appwrite'
|
||||||
import { createMfaApi, isMoreFactorsError, maskEmail, mfaErrorMessage } from '../lib/mfa'
|
import { createMfaApi, isMoreFactorsError, maskEmail, mfaErrorMessage } from '../lib/mfa'
|
||||||
|
import { isStaffAccount, NO_STAFF_ACCESS } from '../lib/staffAccess'
|
||||||
|
|
||||||
const AuthContext = createContext()
|
const AuthContext = createContext()
|
||||||
const mfa = createMfaApi(client)
|
const mfa = createMfaApi(client)
|
||||||
@@ -52,8 +53,16 @@ async function createSessionWithCleanup(email, password) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hilfsfunktion: Fügt User automatisch zur employees Collection hinzu und liefert
|
// Konto ohne Mitarbeiter-Label (z. B. Kunde mit Portal-Passwort): Sitzung sofort beenden
|
||||||
// den Mitarbeiter-Eintrag (Kürzel = Gitea-Name, siehe isWorksheetCreator) oder null
|
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) {
|
async function ensureEmployeeExists(user) {
|
||||||
if (!user) return null
|
if (!user) return null
|
||||||
|
|
||||||
@@ -109,6 +118,11 @@ export function AuthProvider({ children }) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await account.get()
|
const session = await account.get()
|
||||||
|
if (!isStaffAccount(session)) {
|
||||||
|
await endSession()
|
||||||
|
setUser(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
setUser(session)
|
setUser(session)
|
||||||
setEmployee(await ensureEmployeeExists(session))
|
setEmployee(await ensureEmployeeExists(session))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -147,6 +161,10 @@ export function AuthProvider({ children }) {
|
|||||||
const sent = await sendCode()
|
const sent = await sendCode()
|
||||||
return { success: false, mfaRequired: true, error: sent.error }
|
return { success: false, mfaRequired: true, error: sent.error }
|
||||||
}
|
}
|
||||||
|
if (!isStaffAccount(session)) {
|
||||||
|
await endSession()
|
||||||
|
return { success: false, error: NO_STAFF_ACCESS }
|
||||||
|
}
|
||||||
setUser(session)
|
setUser(session)
|
||||||
setEmployee(await ensureEmployeeExists(session))
|
setEmployee(await ensureEmployeeExists(session))
|
||||||
|
|
||||||
@@ -191,6 +209,11 @@ export function AuthProvider({ children }) {
|
|||||||
try {
|
try {
|
||||||
await mfa.completeChallenge(mfaPending.challengeId, String(otp || '').trim())
|
await mfa.completeChallenge(mfaPending.challengeId, String(otp || '').trim())
|
||||||
const session = await account.get()
|
const session = await account.get()
|
||||||
|
if (!isStaffAccount(session)) {
|
||||||
|
await endSession()
|
||||||
|
setMfaPending(null)
|
||||||
|
return { success: false, error: NO_STAFF_ACCESS }
|
||||||
|
}
|
||||||
setUser(session)
|
setUser(session)
|
||||||
setEmployee(await ensureEmployeeExists(session))
|
setEmployee(await ensureEmployeeExists(session))
|
||||||
setMfaPending(null)
|
setMfaPending(null)
|
||||||
|
|||||||
@@ -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}")`))
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { useNavigate, useLocation } from 'react-router-dom'
|
|||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import LetterGlitch from '../components/LetterGlitch'
|
import LetterGlitch from '../components/LetterGlitch'
|
||||||
import { safeReturnPath } from '../lib/returnPath'
|
import { safeReturnPath } from '../lib/returnPath'
|
||||||
|
import { NO_STAFF_ACCESS } from '../lib/staffAccess'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
@@ -33,7 +34,9 @@ export default function LoginPage() {
|
|||||||
// Bessere Fehlermeldungen
|
// Bessere Fehlermeldungen
|
||||||
let errorMessage = result.error || 'Login fehlgeschlagen'
|
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.'
|
errorMessage = 'Ungültige Email oder Passwort. Bitte überprüfe deine Eingaben.'
|
||||||
} else if (errorMessage.includes('Email/Password') || errorMessage.includes('auth')) {
|
} else if (errorMessage.includes('Email/Password') || errorMessage.includes('auth')) {
|
||||||
errorMessage = 'Email/Password Authentifizierung ist möglicherweise nicht aktiviert. Bitte überprüfe deine Appwrite-Konfiguration.'
|
errorMessage = 'Email/Password Authentifizierung ist möglicherweise nicht aktiviert. Bitte überprüfe deine Appwrite-Konfiguration.'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in new issue
Block a user