package store import ( "context" "crypto/sha256" "database/sql" "encoding/hex" "errors" "fmt" "time" ) // hashToken: Tokens werden nur als SHA-256 gespeichert. Ein einfacher Hash // reicht, weil Tokens zufällig mit hoher Entropie erzeugt werden. func hashToken(token string) string { sum := sha256.Sum256([]byte(token)) return hex.EncodeToString(sum[:]) } // AddEnrollmentToken hinterlegt einen einmaligen Einladungs-Token für // tenantID. Erzeugt wird der Token vom Aufrufer; gespeichert wird nur sein // Hash. func (s *Store) AddEnrollmentToken(ctx context.Context, token, tenantID string, expiresAt time.Time) error { if token == "" { return invalid("Token fehlt") } if !expiresAt.After(s.now()) { return invalid("Ablaufzeitpunkt %v liegt nicht in der Zukunft", expiresAt) } _, err := s.db.ExecContext(ctx, `INSERT INTO enrollment_tokens (token_hash, tenant_id, expires_at, used) VALUES (?, ?, ?, 0)`, hashToken(token), tenantID, formatTime(expiresAt)) return mapConstraint(err, "Enrollment-Token") } // Enroll löst token ein und legt in derselben Transaktion den Knoten n an. // n.TenantID wird durch den Mandanten des Tokens ersetzt. Schlägt das Anlegen // fehl, bleibt der Token unverbraucht. Fehler: ErrNotFound (unbekannter // Token), ErrTokenUsed, ErrTokenExpired sowie die Fehler von CreateNode. func (s *Store) Enroll(ctx context.Context, token string, n Node) (Node, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return Node{}, err } defer tx.Rollback() hash := hashToken(token) var tenantID string var expiresAt time.Time var used bool err = tx.QueryRowContext(ctx, `SELECT tenant_id, expires_at, used FROM enrollment_tokens WHERE token_hash = ?`, hash). Scan(&tenantID, dbTime{&expiresAt}, &used) switch { case errors.Is(err, sql.ErrNoRows): return Node{}, fmt.Errorf("Enrollment-Token: %w", ErrNotFound) case err != nil: return Node{}, err case used: return Node{}, ErrTokenUsed case !s.now().Before(expiresAt): return Node{}, ErrTokenExpired } if _, err := tx.ExecContext(ctx, `UPDATE enrollment_tokens SET used = 1, used_at = ? WHERE token_hash = ?`, formatTime(s.now()), hash); err != nil { return Node{}, err } n.TenantID = tenantID node, err := s.insertNode(ctx, tx, n) if err != nil { return Node{}, err } if err := tx.Commit(); err != nil { return Node{}, err } return node, nil } // PurgeEnrollmentTokens löscht Einladungen, die vor mehr als olderThan // eingelöst wurden oder abgelaufen sind (0 = sofort), und liefert ihre Anzahl. func (s *Store) PurgeEnrollmentTokens(ctx context.Context, olderThan time.Duration) (int64, error) { cutoff := formatTime(s.now().Add(-olderThan)) res, err := s.db.ExecContext(ctx, `DELETE FROM enrollment_tokens WHERE (used = 1 AND (used_at IS NULL OR used_at <= ?)) OR expires_at <= ?`, cutoff, cutoff) if err != nil { return 0, err } return res.RowsAffected() }