package store import ( "context" "errors" "fmt" "time" "github.com/jackc/pgx/v5" ) // Session ist eine angemeldete Sitzung. token ist der Primärschlüssel // (das Cookie-Geheimnis selbst) — es gibt bewusst keine separate ID, // eine Session wird immer über ihren Token nachgeschlagen. type Session struct { Token string UserID string ExpiresAt time.Time CreatedAt time.Time } // CreateSession speichert eine neue Sitzung. token muss bereits ein // kryptographisch zufälliges Geheimnis sein (siehe internal/auth). func (s *Store) CreateSession(ctx context.Context, token, userID string, expiresAt time.Time) (Session, error) { var sess Session err := s.Pool.QueryRow(ctx, ` INSERT INTO session (token, user_id, expires_at) VALUES ($1, $2, $3) RETURNING token, user_id, expires_at, created_at `, token, userID, expiresAt).Scan(&sess.Token, &sess.UserID, &sess.ExpiresAt, &sess.CreatedAt) if err != nil { return Session{}, fmt.Errorf("store: create session: %w", err) } return sess, nil } // GetSession liest eine Sitzung anhand ihres Tokens. Liefert // ErrNotFound, wenn der Token unbekannt ist — abgelaufene Sitzungen // werden NICHT automatisch als "nicht gefunden" behandelt, das prüft // der Aufrufer über ExpiresAt (siehe internal/auth), damit die // Unterscheidung "gab es nie" vs. "ist abgelaufen" nicht verloren geht. func (s *Store) GetSession(ctx context.Context, token string) (Session, error) { var sess Session err := s.Pool.QueryRow(ctx, ` SELECT token, user_id, expires_at, created_at FROM session WHERE token = $1 `, token).Scan(&sess.Token, &sess.UserID, &sess.ExpiresAt, &sess.CreatedAt) if errors.Is(err, pgx.ErrNoRows) { return Session{}, ErrNotFound } if err != nil { return Session{}, fmt.Errorf("store: get session: %w", err) } return sess, nil } // DeleteSession beendet eine Sitzung (Logout). func (s *Store) DeleteSession(ctx context.Context, token string) error { if _, err := s.Pool.Exec(ctx, `DELETE FROM session WHERE token = $1`, token); err != nil { return fmt.Errorf("store: delete session: %w", err) } return nil }