package store import ( "context" "errors" "fmt" "time" "github.com/jackc/pgx/v5" ) // ErrNotFound signalisiert, dass eine Abfrage keine Zeile ergeben hat — // abgrenzbar von echten Fehlern (z. B. Verbindungsabbruch), damit // Aufrufer "nicht gefunden" gezielt anders behandeln können (z. B. bei // Login: falsche E-Mail vs. Datenbankfehler). var ErrNotFound = errors.New("store: nicht gefunden") // User ist ein Login innerhalb eines Account (Mandanten). Nutzer werden // nicht gelöscht (app_user wird von antrag/session/entscheidung/ // audit_log per Foreign Key referenziert — ein Hard-Delete würde die // Historie zerstören), sondern über Active deaktiviert. type User struct { ID string AccountID string Email string PasswordHash string Role string Active bool CreatedAt time.Time } const userColumns = `id, account_id, email, password_hash, role, active, created_at` func scanUser(row interface { Scan(dest ...any) error }) (User, error) { var u User err := row.Scan(&u.ID, &u.AccountID, &u.Email, &u.PasswordHash, &u.Role, &u.Active, &u.CreatedAt) return u, err } // CreateUser legt einen neuen Nutzer innerhalb eines Accounts an. // passwordHash muss bereits gehasht sein (siehe internal/auth) — store // speichert nur, es hasht nicht selbst. func (s *Store) CreateUser(ctx context.Context, accountID, email, passwordHash, role string) (User, error) { row := s.db(ctx).QueryRow(ctx, ` INSERT INTO app_user (account_id, email, password_hash, role) VALUES ($1, $2, $3, $4) RETURNING `+userColumns, accountID, email, passwordHash, role, ) u, err := scanUser(row) if err != nil { return User{}, fmt.Errorf("store: create user: %w", err) } return u, nil } // GetUserByEmail liest einen Nutzer anhand seiner E-Mail-Adresse. // Liefert ErrNotFound, wenn keine E-Mail passt (kein Datenbankfehler). func (s *Store) GetUserByEmail(ctx context.Context, email string) (User, error) { row := s.db(ctx).QueryRow(ctx, `SELECT `+userColumns+` FROM app_user WHERE email = $1`, email) u, err := scanUser(row) if errors.Is(err, pgx.ErrNoRows) { return User{}, ErrNotFound } if err != nil { return User{}, fmt.Errorf("store: get user by email: %w", err) } return u, nil } // GetUser liest einen Nutzer anhand seiner ID. func (s *Store) GetUser(ctx context.Context, id string) (User, error) { row := s.db(ctx).QueryRow(ctx, `SELECT `+userColumns+` FROM app_user WHERE id = $1`, id) u, err := scanUser(row) if errors.Is(err, pgx.ErrNoRows) { return User{}, ErrNotFound } if err != nil { return User{}, fmt.Errorf("store: get user: %w", err) } return u, nil } // ListUsersForAccount liefert alle Logins eines Mandanten — für den // Admin-Bereich (Account-Detailansicht). func (s *Store) ListUsersForAccount(ctx context.Context, accountID string) ([]User, error) { rows, err := s.db(ctx).Query(ctx, ` SELECT `+userColumns+` FROM app_user WHERE account_id = $1 ORDER BY created_at `, accountID) if err != nil { return nil, fmt.Errorf("store: list users for account: %w", err) } defer rows.Close() var out []User for rows.Next() { u, err := scanUser(rows) if err != nil { return nil, fmt.Errorf("store: scan user: %w", err) } out = append(out, u) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("store: list users for account: %w", err) } return out, nil } // SetUserPassword ersetzt den Passwort-Hash eines Nutzers (Passwort- // Zurücksetzen). passwordHash muss bereits gehasht sein, wie bei // CreateUser. func (s *Store) SetUserPassword(ctx context.Context, id, passwordHash string) error { tag, err := s.db(ctx).Exec(ctx, `UPDATE app_user SET password_hash = $2 WHERE id = $1`, id, passwordHash) if err != nil { return fmt.Errorf("store: set user password: %w", err) } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } // SetUserActive (de-)aktiviert einen Login. Ein deaktivierter Nutzer // kann sich nicht mehr anmelden (siehe web.handleLogin), bleibt aber // als Akteur in bestehenden Anträgen/Entscheidungen/Audit-Log-Einträgen // nachvollziehbar — deshalb (de-)aktivieren statt löschen. func (s *Store) SetUserActive(ctx context.Context, id string, active bool) error { tag, err := s.db(ctx).Exec(ctx, `UPDATE app_user SET active = $2 WHERE id = $1`, id, active) if err != nil { return fmt.Errorf("store: set user active: %w", err) } if tag.RowsAffected() == 0 { return ErrNotFound } return nil }