v1.1: Rebrand Hinterstein Immobilien + echtes Auth-System
- Rebrand: Kunden-Logo, Joseph-Hinterstein-Portrait, Kontakt-Banner, Texte - Login-Pflicht auf /login (Route-Guard), Gruppen Verwaltung/Mieter - Mieter-Registrierung per einmaligem Registrierungscode - Benutzerverwaltung als 6. Modul (/benutzer, nur Verwaltung) - Demo-Zugänge mit Passwort 0815 (j.hinterstein, a.berger, m.wolf) - Mail-Server-API: Token-Auth (/send, X-Api-Token), Mail-Stack live auf Server - Store-Key hinterstein-mieterportal-v2, Doku (kimi.md, README) nachgeführt
This commit is contained in:
+272
-21
@@ -6,7 +6,17 @@ import {
|
||||
useReducer,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import type { Role, Ticket, Transaction, DocumentItem, Thread, Message } from '../types'
|
||||
import type {
|
||||
Role,
|
||||
Ticket,
|
||||
Transaction,
|
||||
DocumentItem,
|
||||
Thread,
|
||||
Message,
|
||||
Benutzer,
|
||||
BenutzerRolle,
|
||||
RegistrierungsCode,
|
||||
} from '../types'
|
||||
import {
|
||||
units,
|
||||
transactions,
|
||||
@@ -17,41 +27,109 @@ import {
|
||||
DEMO_TENANT,
|
||||
DEMO_UNIT_ID,
|
||||
} from '../data/seed'
|
||||
import {
|
||||
sha256,
|
||||
DEMO_PASSWORT,
|
||||
ladeSession,
|
||||
speichereSession,
|
||||
beendeSession,
|
||||
type Session,
|
||||
} from '../lib/auth'
|
||||
|
||||
const DEMO_HASH = 'c57a5df4f97ed6914116e5fd1348406064834f22503d890eb571af442ac4b4c3' // sha256("0815")
|
||||
|
||||
/** 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 {
|
||||
role: Role
|
||||
tickets: Ticket[]
|
||||
transactions: Transaction[]
|
||||
documents: DocumentItem[]
|
||||
threads: Thread[]
|
||||
benutzer: Benutzer[]
|
||||
codes: RegistrierungsCode[]
|
||||
session: Session | null
|
||||
tick: number
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'setRole'; role: Role }
|
||||
| { type: 'addTicket'; ticket: Ticket }
|
||||
| { type: 'setTicketStatus'; id: string; status: Ticket['status'] }
|
||||
| {
|
||||
type: 'addMessage'
|
||||
threadId: string
|
||||
message: Message
|
||||
}
|
||||
| { 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 = {
|
||||
role: 'tenant',
|
||||
tickets,
|
||||
transactions,
|
||||
documents,
|
||||
threads,
|
||||
benutzer: demoBenutzer,
|
||||
codes: demoCodes,
|
||||
session: ladeSession(),
|
||||
tick: 0,
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'hocks-mieterportal-v1'
|
||||
const STORAGE_KEY = 'hinterstein-mieterportal-v2'
|
||||
|
||||
function loadState(): State {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (raw) return { ...initialState, ...JSON.parse(raw) }
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw)
|
||||
return { ...initialState, ...parsed, session: ladeSession() }
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -60,8 +138,6 @@ function loadState(): State {
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'setRole':
|
||||
return { ...state, role: action.role }
|
||||
case 'addTicket':
|
||||
return { ...state, tickets: [action.ticket, ...state.tickets] }
|
||||
case 'setTicketStatus':
|
||||
@@ -98,16 +174,86 @@ function reducer(state: State, action: Action): State {
|
||||
: 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>
|
||||
demoUnit: typeof units[number]
|
||||
units: typeof units
|
||||
announcements: typeof announcements
|
||||
/** Aktuell angemeldeter Benutzer (null = nicht eingeloggt). */
|
||||
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]
|
||||
login: (benutzername: string, passwort: string) => Promise<string | null>
|
||||
logout: () => void
|
||||
registrieren: (
|
||||
code: string,
|
||||
benutzername: string,
|
||||
name: string,
|
||||
email: string,
|
||||
passwort: string,
|
||||
) => Promise<string | null>
|
||||
benutzerAnlegen: (
|
||||
benutzername: string,
|
||||
name: string,
|
||||
rolle: BenutzerRolle,
|
||||
email: string,
|
||||
passwort: string,
|
||||
einheitId: string | null,
|
||||
) => Promise<string | null>
|
||||
setAktiv: (id: string, aktiv: boolean) => void
|
||||
setPasswort: (id: string, passwort: string) => Promise<void>
|
||||
weiseEinheitZu: (id: string, einheitId: string | null) => void
|
||||
generiereCodes: (anzahl: number, einheitId: string | null) => RegistrierungsCode[]
|
||||
}
|
||||
|
||||
const StoreContext = createContext<Store | null>(null)
|
||||
@@ -119,16 +265,121 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
||||
}, [state])
|
||||
|
||||
const value = useMemo<Store>(
|
||||
() => ({
|
||||
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'),
|
||||
}
|
||||
dispatch({ type: 'registriert', benutzer: b, code: c.code })
|
||||
speichereSession(b.id, b.rolle)
|
||||
dispatch({ type: 'setSession', session: ladeSession() })
|
||||
return null
|
||||
}
|
||||
|
||||
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'),
|
||||
}
|
||||
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'),
|
||||
}))
|
||||
dispatch({ type: 'codesHinzugefuegt', codes: neu })
|
||||
return neu
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
dispatch,
|
||||
demoUnit: units.find((u) => u.id === DEMO_UNIT_ID)!,
|
||||
units,
|
||||
announcements,
|
||||
}),
|
||||
[state],
|
||||
)
|
||||
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])
|
||||
|
||||
return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>
|
||||
}
|
||||
@@ -144,4 +395,4 @@ export const fmtEUR = new Intl.NumberFormat('de-DE', {
|
||||
currency: 'EUR',
|
||||
})
|
||||
|
||||
export { DEMO_TENANT, DEMO_UNIT_ID }
|
||||
export { DEMO_TENANT, DEMO_UNIT_ID, DEMO_PASSWORT }
|
||||
|
||||
Reference in New Issue
Block a user