v1.3: Serverseitige Datenbank (PostgreSQL + Node-API) ersetzt localStorage
- server/: Express-API (Auth, Bootstrap, Tickets, Threads, Benutzerverwaltung, Codes) mit pg-Schema (11 Tabellen), idempotem Seed (50 Wohnungen, 552 Buchungen) - mp-db (PostgreSQL 16, Volume mp_pgdata) + mp-api im Compose-Stack, nginx proxiet /api/ an mp-api; deploy.sh baut API-Image auf dem Server - Frontend: Store laedt /api/bootstrap, Schreibzugriff async, kein localStorage mehr (ausser Theme), Ladebildschirm beim Session-Check, vite-Proxy fuer Dev - Rollenrechte serverseitig: Mieter nur eigene Wohnung (getestet, 403-Verweis) - Mail-Benachrichtigungen komplett serverseitig (server/src/mail.js) - beispieldaten.py schreibt server/seed/generated.json; Verwaltungs-Mail = axel@hocks.eu - E2E live verifiziert: Login, Scoping, Ticket + Zustellung (status=sent), Registrierung, Codes; Testdaten bereinigt; Doku nachgefuehrt
This commit is contained in:
+204
-324
@@ -1,240 +1,72 @@
|
||||
/** Zentraler Store des Mieterportals – seit v1.3 komplett serverseitig
|
||||
* (Node-API + PostgreSQL). Der Browser hält keine Mietdaten mehr im
|
||||
* localStorage; die Sitzung läuft über ein HttpOnly-Cookie (8 h).
|
||||
*
|
||||
* Das Store-Interface bleibt absichtlich nahe an der bisherigen Form, damit
|
||||
* die Seiten nur minimal angepasst werden mussten: Lesezugriff synchron über
|
||||
* den State, Schreibzugriff über asynchrone Methoden (rufen die API auf und
|
||||
* aktualisieren danach den lokalen State aus der Antwort/Neuladung).
|
||||
*/
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import type {
|
||||
Role,
|
||||
Ticket,
|
||||
TicketStatus,
|
||||
Transaction,
|
||||
DocumentItem,
|
||||
Thread,
|
||||
Message,
|
||||
Benutzer,
|
||||
BenutzerRolle,
|
||||
RegistrierungsCode,
|
||||
Unit,
|
||||
Announcement,
|
||||
} from '../types'
|
||||
import {
|
||||
units,
|
||||
transactions,
|
||||
tickets,
|
||||
documents,
|
||||
threads,
|
||||
announcements,
|
||||
DEMO_TENANT,
|
||||
DEMO_UNIT_ID,
|
||||
} from '../data/seed'
|
||||
import {
|
||||
sha256,
|
||||
DEMO_PASSWORT,
|
||||
ladeSession,
|
||||
speichereSession,
|
||||
beendeSession,
|
||||
type Session,
|
||||
} from '../lib/auth'
|
||||
import { apiGet, apiPost, apiPatch, ApiFehler } from '../lib/api'
|
||||
import { sha256, DEMO_PASSWORT } from '../lib/auth'
|
||||
|
||||
const DEMO_HASH = 'c57a5df4f97ed6914116e5fd1348406064834f22503d890eb571af442ac4b4c3' // sha256("0815")
|
||||
/** Demo-Einheit als Fallback, falls ein Mieter noch keine Wohnung zugewiesen hat. */
|
||||
export const DEMO_TENANT = 'Anna Berger'
|
||||
export const DEMO_UNIT_ID = 'hs-7'
|
||||
|
||||
/** Demo-Benutzer (Passwort aller: 0815). */
|
||||
export const demoBenutzer: Benutzer[] = [
|
||||
{
|
||||
id: 'b-joseph',
|
||||
benutzername: 'j.hinterstein',
|
||||
name: 'Joseph Hinterstein',
|
||||
passwortHash: DEMO_HASH,
|
||||
rolle: 'verwaltung',
|
||||
email: 'joseph@hinterstein-immo.de',
|
||||
einheitId: null,
|
||||
aktiv: true,
|
||||
erstelltAm: '01.03.2024',
|
||||
},
|
||||
{
|
||||
id: 'b-anna',
|
||||
benutzername: 'a.berger',
|
||||
name: DEMO_TENANT,
|
||||
passwortHash: DEMO_HASH,
|
||||
rolle: 'mieter',
|
||||
email: 'anna.berger@example.de',
|
||||
einheitId: DEMO_UNIT_ID,
|
||||
aktiv: true,
|
||||
erstelltAm: '01.03.2024',
|
||||
letzterLogin: '02.10.2026, 09:12',
|
||||
},
|
||||
{
|
||||
id: 'b-jonas',
|
||||
benutzername: 'm.wolf',
|
||||
name: 'Jonas Klein',
|
||||
passwortHash: DEMO_HASH,
|
||||
rolle: 'mieter',
|
||||
email: 'jonas.klein@example.de',
|
||||
einheitId: 'rw-4',
|
||||
aktiv: true,
|
||||
erstelltAm: '15.08.2025',
|
||||
},
|
||||
]
|
||||
|
||||
/** Demo-Registrierungscode (Verwaltung kann weitere erzeugen). */
|
||||
const demoCodes: RegistrierungsCode[] = [
|
||||
{
|
||||
code: 'MP-2026-0815',
|
||||
einheitId: 'ap-2',
|
||||
erstelltAm: '02.10.2026',
|
||||
},
|
||||
]
|
||||
|
||||
interface State {
|
||||
tickets: Ticket[]
|
||||
interface Bootstrap {
|
||||
units: Unit[]
|
||||
transactions: Transaction[]
|
||||
documents: DocumentItem[]
|
||||
tickets: Ticket[]
|
||||
threads: Thread[]
|
||||
announcements: Announcement[]
|
||||
benutzer: Benutzer[]
|
||||
codes: RegistrierungsCode[]
|
||||
session: Session | null
|
||||
tick: number
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'addTicket'; ticket: Ticket }
|
||||
| { type: 'setTicketStatus'; id: string; status: Ticket['status'] }
|
||||
| { type: 'addMessage'; threadId: string; message: Message }
|
||||
| { type: 'newThread'; thread: Thread }
|
||||
| { type: 'markThreadRead'; threadId: string; role: Role }
|
||||
| { type: 'setSession'; session: Session | null }
|
||||
| { type: 'registriert'; benutzer: Benutzer; code: string }
|
||||
| { type: 'benutzerAngelegt'; benutzer: Benutzer }
|
||||
| { type: 'benutzerAktiv'; id: string; aktiv: boolean }
|
||||
| { type: 'passwortGesetzt'; id: string; hash: string }
|
||||
| { type: 'einheitZugewiesen'; id: string; einheitId: string | null }
|
||||
| { type: 'codesHinzugefuegt'; codes: RegistrierungsCode[] }
|
||||
|
||||
const initialState: State = {
|
||||
tickets,
|
||||
transactions,
|
||||
documents,
|
||||
threads,
|
||||
benutzer: demoBenutzer,
|
||||
codes: demoCodes,
|
||||
session: ladeSession(),
|
||||
tick: 0,
|
||||
const LEER: Bootstrap = {
|
||||
units: [],
|
||||
transactions: [],
|
||||
documents: [],
|
||||
tickets: [],
|
||||
threads: [],
|
||||
announcements: [],
|
||||
benutzer: [],
|
||||
codes: [],
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'hinterstein-mieterportal-v2'
|
||||
|
||||
function loadState(): State {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw)
|
||||
return { ...initialState, ...parsed, session: ladeSession() }
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return initialState
|
||||
}
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'addTicket':
|
||||
return { ...state, tickets: [action.ticket, ...state.tickets] }
|
||||
case 'setTicketStatus':
|
||||
return {
|
||||
...state,
|
||||
tickets: state.tickets.map((t) =>
|
||||
t.id === action.id ? { ...t, status: action.status } : t,
|
||||
),
|
||||
}
|
||||
case 'addMessage':
|
||||
return {
|
||||
...state,
|
||||
threads: state.threads.map((th) =>
|
||||
th.id === action.threadId
|
||||
? {
|
||||
...th,
|
||||
unreadByAdmin: action.message.from === 'tenant',
|
||||
unreadByTenant: action.message.from === 'admin',
|
||||
messages: [...th.messages, action.message],
|
||||
}
|
||||
: th,
|
||||
),
|
||||
}
|
||||
case 'newThread':
|
||||
return { ...state, threads: [action.thread, ...state.threads] }
|
||||
case 'markThreadRead':
|
||||
return {
|
||||
...state,
|
||||
threads: state.threads.map((th) =>
|
||||
th.id === action.threadId
|
||||
? action.role === 'tenant'
|
||||
? { ...th, unreadByTenant: false }
|
||||
: { ...th, unreadByAdmin: false }
|
||||
: th,
|
||||
),
|
||||
}
|
||||
case 'setSession':
|
||||
return { ...state, session: action.session, tick: state.tick + 1 }
|
||||
case 'registriert':
|
||||
return {
|
||||
...state,
|
||||
benutzer: [...state.benutzer, action.benutzer],
|
||||
codes: state.codes.map((c) =>
|
||||
c.code === action.code ? { ...c, verwendetAm: new Date().toLocaleDateString('de-DE') } : c,
|
||||
),
|
||||
}
|
||||
case 'benutzerAngelegt':
|
||||
return { ...state, benutzer: [...state.benutzer, action.benutzer] }
|
||||
case 'benutzerAktiv':
|
||||
return {
|
||||
...state,
|
||||
benutzer: state.benutzer.map((b) =>
|
||||
b.id === action.id ? { ...b, aktiv: action.aktiv } : b,
|
||||
),
|
||||
}
|
||||
case 'passwortGesetzt':
|
||||
return {
|
||||
...state,
|
||||
benutzer: state.benutzer.map((b) =>
|
||||
b.id === action.id ? { ...b, passwortHash: action.hash } : b,
|
||||
),
|
||||
}
|
||||
case 'einheitZugewiesen':
|
||||
return {
|
||||
...state,
|
||||
benutzer: state.benutzer.map((b) =>
|
||||
b.id === action.id ? { ...b, einheitId: action.einheitId } : b,
|
||||
),
|
||||
}
|
||||
case 'codesHinzugefuegt':
|
||||
return { ...state, codes: [...action.codes, ...state.codes] }
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function neuerCode(): string {
|
||||
const teile = () =>
|
||||
Array.from(crypto.getRandomValues(new Uint8Array(2)))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
return `MP-${teile()}-${teile()}`
|
||||
}
|
||||
|
||||
interface Store extends State {
|
||||
dispatch: React.Dispatch<Action>
|
||||
units: typeof units
|
||||
announcements: typeof announcements
|
||||
/** Aktuell angemeldeter Benutzer (null = nicht eingeloggt). */
|
||||
interface Store extends Bootstrap {
|
||||
/** Erster /api/auth/me-Lauf ist abgeschlossen (sonst Ladebildschirm). */
|
||||
bereit: boolean
|
||||
me: Benutzer | null
|
||||
/** Abgeleitete Portal-Rolle für die Modul-Ansichten. */
|
||||
role: Role
|
||||
/** Einheit des angemeldeten Mieters (Demo-Fallback hs-7). */
|
||||
meineEinheit: (typeof units)[number]
|
||||
meineEinheit: Unit
|
||||
login: (benutzername: string, passwort: string) => Promise<string | null>
|
||||
logout: () => void
|
||||
logout: () => Promise<void>
|
||||
registrieren: (
|
||||
code: string,
|
||||
benutzername: string,
|
||||
@@ -242,6 +74,11 @@ interface Store extends State {
|
||||
email: string,
|
||||
passwort: string,
|
||||
) => Promise<string | null>
|
||||
addTicket: (t: { title: string; description: string; category: string; priority: string; unitId?: string }) => Promise<string | null>
|
||||
setTicketStatus: (id: string, status: TicketStatus) => Promise<void>
|
||||
newThread: (subject: string, text: string, unitId?: string) => Promise<string | null>
|
||||
addMessage: (threadId: string, text: string) => Promise<void>
|
||||
markThreadRead: (threadId: string) => void
|
||||
benutzerAnlegen: (
|
||||
benutzername: string,
|
||||
name: string,
|
||||
@@ -250,136 +87,179 @@ interface Store extends State {
|
||||
passwort: string,
|
||||
einheitId: string | null,
|
||||
) => Promise<string | null>
|
||||
setAktiv: (id: string, aktiv: boolean) => void
|
||||
setAktiv: (id: string, aktiv: boolean) => Promise<void>
|
||||
setPasswort: (id: string, passwort: string) => Promise<void>
|
||||
weiseEinheitZu: (id: string, einheitId: string | null) => void
|
||||
generiereCodes: (anzahl: number, einheitId: string | null) => RegistrierungsCode[]
|
||||
weiseEinheitZu: (id: string, einheitId: string | null) => Promise<void>
|
||||
generiereCodes: (anzahl: number, einheitId: string | null) => Promise<RegistrierungsCode[]>
|
||||
}
|
||||
|
||||
const StoreContext = createContext<Store | null>(null)
|
||||
|
||||
export function StoreProvider({ children }: { children: ReactNode }) {
|
||||
const [state, dispatch] = useReducer(reducer, undefined, loadState)
|
||||
const [bereit, setBereit] = useState(false)
|
||||
const [me, setMe] = useState<Benutzer | null>(null)
|
||||
const [daten, setDaten] = useState<Bootstrap>(LEER)
|
||||
|
||||
/** Lädt alle Stammdaten neu (nach Login/Registrierung und nach Schreibaktionen). */
|
||||
const ladeAlles = useCallback(async () => {
|
||||
const b = await apiGet<Bootstrap>('/api/bootstrap')
|
||||
setDaten(b)
|
||||
}, [])
|
||||
|
||||
// Start: bestehende Session prüfen, dann Daten laden
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
||||
}, [state])
|
||||
|
||||
const value = useMemo<Store>(() => {
|
||||
const me =
|
||||
(state.session &&
|
||||
state.benutzer.find((b) => b.id === state.session!.benutzerId && b.aktiv)) ||
|
||||
null
|
||||
const role: Role = me?.rolle === 'verwaltung' ? 'admin' : 'tenant'
|
||||
const meineEinheit =
|
||||
units.find((u) => u.id === (me?.einheitId ?? DEMO_UNIT_ID)) ??
|
||||
units.find((u) => u.id === DEMO_UNIT_ID)!
|
||||
|
||||
const login = async (benutzername: string, passwort: string) => {
|
||||
const b = state.benutzer.find(
|
||||
(x) => x.benutzername.toLowerCase() === benutzername.trim().toLowerCase(),
|
||||
)
|
||||
if (!b) return 'Benutzername oder Passwort falsch.'
|
||||
if (!b.aktiv) return 'Dieses Konto ist deaktiviert – bitte die Verwaltung kontaktieren.'
|
||||
if ((await sha256(passwort)) !== b.passwortHash)
|
||||
return 'Benutzername oder Passwort falsch.'
|
||||
speichereSession(b.id, b.rolle)
|
||||
dispatch({ type: 'setSession', session: ladeSession() })
|
||||
return null
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
beendeSession()
|
||||
dispatch({ type: 'setSession', session: null })
|
||||
}
|
||||
|
||||
const registrieren = async (
|
||||
code: string,
|
||||
benutzername: string,
|
||||
name: string,
|
||||
email: string,
|
||||
passwort: string,
|
||||
) => {
|
||||
const c = state.codes.find(
|
||||
(x) => x.code.toUpperCase() === code.trim().toUpperCase() && !x.verwendetAm,
|
||||
)
|
||||
if (!c) return 'Der Registrierungscode ist ungültig oder bereits verwendet.'
|
||||
if (state.benutzer.some((x) => x.benutzername.toLowerCase() === benutzername.trim().toLowerCase()))
|
||||
return 'Dieser Benutzername ist bereits vergeben.'
|
||||
const b: Benutzer = {
|
||||
id: `b-${Date.now()}`,
|
||||
benutzername: benutzername.trim(),
|
||||
name: name.trim(),
|
||||
passwortHash: await sha256(passwort),
|
||||
rolle: 'mieter',
|
||||
email: email.trim(),
|
||||
einheitId: c.einheitId,
|
||||
aktiv: true,
|
||||
erstelltAm: new Date().toLocaleDateString('de-DE'),
|
||||
let aktiv = true
|
||||
;(async () => {
|
||||
try {
|
||||
const { benutzer } = await apiGet<{ benutzer: Benutzer }>('/api/auth/me')
|
||||
if (!aktiv) return
|
||||
setMe(benutzer)
|
||||
await ladeAlles()
|
||||
} catch {
|
||||
/* nicht angemeldet – Login-Seite zeigen */
|
||||
} finally {
|
||||
if (aktiv) setBereit(true)
|
||||
}
|
||||
dispatch({ type: 'registriert', benutzer: b, code: c.code })
|
||||
speichereSession(b.id, b.rolle)
|
||||
dispatch({ type: 'setSession', session: ladeSession() })
|
||||
return null
|
||||
})()
|
||||
return () => {
|
||||
aktiv = false
|
||||
}
|
||||
}, [ladeAlles])
|
||||
|
||||
const benutzerAnlegen = async (
|
||||
benutzername: string,
|
||||
name: string,
|
||||
rolle: BenutzerRolle,
|
||||
email: string,
|
||||
passwort: string,
|
||||
einheitId: string | null,
|
||||
) => {
|
||||
if (state.benutzer.some((x) => x.benutzername.toLowerCase() === benutzername.trim().toLowerCase()))
|
||||
return 'Dieser Benutzername ist bereits vergeben.'
|
||||
const b: Benutzer = {
|
||||
id: `b-${Date.now()}`,
|
||||
benutzername: benutzername.trim(),
|
||||
name: name.trim(),
|
||||
passwortHash: await sha256(passwort),
|
||||
rolle,
|
||||
email: email.trim(),
|
||||
einheitId: rolle === 'mieter' ? einheitId : null,
|
||||
aktiv: true,
|
||||
erstelltAm: new Date().toLocaleDateString('de-DE'),
|
||||
const role: Role = me?.rolle === 'verwaltung' ? 'admin' : 'tenant'
|
||||
const meineEinheit =
|
||||
daten.units.find((u) => u.id === (me?.einheitId ?? DEMO_UNIT_ID)) ??
|
||||
daten.units.find((u) => u.id === DEMO_UNIT_ID) ??
|
||||
daten.units[0]
|
||||
|
||||
const fehlerNachricht = (e: unknown): string =>
|
||||
e instanceof ApiFehler ? e.message : 'Ein unerwarteter Fehler ist aufgetreten.'
|
||||
|
||||
const value = useMemo<Store>(() => ({
|
||||
...daten,
|
||||
bereit,
|
||||
me,
|
||||
role,
|
||||
meineEinheit,
|
||||
// ── Auth ────────────────────────────────────────────────
|
||||
async login(benutzername, passwort) {
|
||||
try {
|
||||
const { benutzer } = await apiPost<{ benutzer: Benutzer }>('/api/auth/login', {
|
||||
benutzername,
|
||||
passwortHash: await sha256(passwort),
|
||||
})
|
||||
setMe(benutzer)
|
||||
await ladeAlles()
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
dispatch({ type: 'benutzerAngelegt', benutzer: b })
|
||||
return null
|
||||
}
|
||||
|
||||
const setPasswort = async (id: string, passwort: string) => {
|
||||
dispatch({ type: 'passwortGesetzt', id, hash: await sha256(passwort) })
|
||||
}
|
||||
|
||||
const generiereCodes = (anzahl: number, einheitId: string | null) => {
|
||||
const neu: RegistrierungsCode[] = Array.from({ length: anzahl }, () => ({
|
||||
code: neuerCode(),
|
||||
einheitId,
|
||||
erstelltAm: new Date().toLocaleDateString('de-DE'),
|
||||
},
|
||||
async logout() {
|
||||
try {
|
||||
await apiPost('/api/auth/logout')
|
||||
} catch {
|
||||
/* Session ggf. bereits ungültig – trotzdem lokal abmelden */
|
||||
}
|
||||
setMe(null)
|
||||
setDaten(LEER)
|
||||
},
|
||||
async registrieren(code, benutzername, name, email, passwort) {
|
||||
try {
|
||||
const { benutzer } = await apiPost<{ benutzer: Benutzer }>('/api/auth/register', {
|
||||
code,
|
||||
benutzername,
|
||||
name,
|
||||
email,
|
||||
passwortHash: await sha256(passwort),
|
||||
})
|
||||
setMe(benutzer)
|
||||
await ladeAlles()
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
// ── Schadensmeldungen ───────────────────────────────────
|
||||
async addTicket(t) {
|
||||
try {
|
||||
await apiPost('/api/tickets', t)
|
||||
await ladeAlles()
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
async setTicketStatus(id, status) {
|
||||
const { ticket } = await apiPatch<{ ticket: Ticket }>(`/api/tickets/${id}`, { status })
|
||||
setDaten((d) => ({ ...d, tickets: d.tickets.map((t) => (t.id === id ? ticket : t)) }))
|
||||
},
|
||||
// ── Nachrichten ─────────────────────────────────────────
|
||||
async newThread(subject, text, unitId) {
|
||||
try {
|
||||
const { thread } = await apiPost<{ thread: Thread }>('/api/threads', { subject, text, unitId })
|
||||
setDaten((d) => ({ ...d, threads: [thread, ...d.threads.filter((t) => t.id !== thread.id)] }))
|
||||
return thread.id
|
||||
} catch (e) {
|
||||
console.error(fehlerNachricht(e))
|
||||
return null
|
||||
}
|
||||
},
|
||||
async addMessage(threadId, text) {
|
||||
const { thread } = await apiPost<{ thread: Thread }>(`/api/threads/${threadId}/messages`, { text })
|
||||
setDaten((d) => ({ ...d, threads: d.threads.map((t) => (t.id === threadId ? thread : t)) }))
|
||||
},
|
||||
markThreadRead(threadId) {
|
||||
// Optimistisch lokal markieren, serverseitig nachmelden
|
||||
setDaten((d) => ({
|
||||
...d,
|
||||
threads: d.threads.map((t) =>
|
||||
t.id === threadId
|
||||
? role === 'tenant'
|
||||
? { ...t, unreadByTenant: false }
|
||||
: { ...t, unreadByAdmin: false }
|
||||
: t,
|
||||
),
|
||||
}))
|
||||
dispatch({ type: 'codesHinzugefuegt', codes: neu })
|
||||
return neu
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
dispatch,
|
||||
units,
|
||||
announcements,
|
||||
me,
|
||||
role,
|
||||
meineEinheit,
|
||||
login,
|
||||
logout,
|
||||
registrieren,
|
||||
benutzerAnlegen,
|
||||
setAktiv: (id, aktiv) => dispatch({ type: 'benutzerAktiv', id, aktiv }),
|
||||
setPasswort,
|
||||
weiseEinheitZu: (id, einheitId) => dispatch({ type: 'einheitZugewiesen', id, einheitId }),
|
||||
generiereCodes,
|
||||
}
|
||||
}, [state])
|
||||
apiPost(`/api/threads/${threadId}/read`).catch(() => {})
|
||||
},
|
||||
// ── Benutzerverwaltung (Verwaltung) ─────────────────────
|
||||
async benutzerAnlegen(benutzername, name, rolle, email, passwort, einheitId) {
|
||||
try {
|
||||
await apiPost('/api/users', {
|
||||
benutzername,
|
||||
name,
|
||||
rolle,
|
||||
email,
|
||||
einheitId,
|
||||
passwortHash: await sha256(passwort),
|
||||
})
|
||||
await ladeAlles()
|
||||
return null
|
||||
} catch (e) {
|
||||
return fehlerNachricht(e)
|
||||
}
|
||||
},
|
||||
async setAktiv(id, aktiv) {
|
||||
const { benutzer } = await apiPatch<{ benutzer: Benutzer }>(`/api/users/${id}`, { aktiv })
|
||||
setDaten((d) => ({ ...d, benutzer: d.benutzer.map((b) => (b.id === id ? benutzer : b)) }))
|
||||
},
|
||||
async setPasswort(id, passwort) {
|
||||
const { benutzer } = await apiPatch<{ benutzer: Benutzer }>(`/api/users/${id}`, {
|
||||
passwortHash: await sha256(passwort),
|
||||
})
|
||||
setDaten((d) => ({ ...d, benutzer: d.benutzer.map((b) => (b.id === id ? benutzer : b)) }))
|
||||
},
|
||||
async weiseEinheitZu(id, einheitId) {
|
||||
const { benutzer } = await apiPatch<{ benutzer: Benutzer }>(`/api/users/${id}`, { einheitId })
|
||||
setDaten((d) => ({ ...d, benutzer: d.benutzer.map((b) => (b.id === id ? benutzer : b)) }))
|
||||
},
|
||||
async generiereCodes(anzahl, einheitId) {
|
||||
const { codes } = await apiPost<{ codes: RegistrierungsCode[] }>('/api/codes', { anzahl, einheitId })
|
||||
await ladeAlles()
|
||||
return codes
|
||||
},
|
||||
}), [daten, bereit, me, role, meineEinheit, ladeAlles])
|
||||
|
||||
return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>
|
||||
}
|
||||
@@ -395,4 +275,4 @@ export const fmtEUR = new Intl.NumberFormat('de-DE', {
|
||||
currency: 'EUR',
|
||||
})
|
||||
|
||||
export { DEMO_TENANT, DEMO_UNIT_ID, DEMO_PASSWORT }
|
||||
export { DEMO_PASSWORT }
|
||||
|
||||
Reference in New Issue
Block a user