package cluster import ( "context" "database/sql" "errors" "fmt" "strings" "time" "forgejo.digital-droplets.de/philschlo/proxui/platform/encryption" ) type Cluster struct { ID string Name string APIEndpoint string TLSFingerprint string TokenID string TokenSecret string Status string CreatedAt time.Time } type StoredCluster struct { ID string Name string APIEndpoint string TLSFingerprint string EncryptedToken []byte TokenID string Status string CreatedAt time.Time } type Storage interface { Get(ctx context.Context, id string) (StoredCluster, bool, error) Upsert(ctx context.Context, cluster StoredCluster) (StoredCluster, error) SetStatus(ctx context.Context, id string, status string) (StoredCluster, bool, error) } type Repository struct { storage Storage cipher encryption.Cipher } func NewRepository(db *sql.DB, cipher encryption.Cipher) Repository { return Repository{ storage: SQLStorage{db: db}, cipher: cipher, } } func NewRepositoryWithStorage(storage Storage, cipher encryption.Cipher) Repository { return Repository{ storage: storage, cipher: cipher, } } func (r Repository) GetCluster(ctx context.Context, id string) (Cluster, bool, error) { stored, found, err := r.storage.Get(ctx, id) if err != nil || !found { return Cluster{}, found, err } tokenSecret, err := r.cipher.Decrypt(stored.EncryptedToken) if err != nil { return Cluster{}, false, err } return Cluster{ ID: stored.ID, Name: stored.Name, APIEndpoint: stored.APIEndpoint, TLSFingerprint: stored.TLSFingerprint, TokenID: stored.TokenID, TokenSecret: string(tokenSecret), Status: stored.Status, CreatedAt: stored.CreatedAt, }, true, nil } func (r Repository) UpsertCluster(ctx context.Context, cluster Cluster) (Cluster, error) { if strings.TrimSpace(cluster.TokenSecret) == "" { return Cluster{}, fmt.Errorf("token secret is required") } encryptedToken, err := r.cipher.Encrypt([]byte(cluster.TokenSecret)) if err != nil { return Cluster{}, err } status := strings.TrimSpace(cluster.Status) if status == "" { status = "active" } stored, err := r.storage.Upsert(ctx, StoredCluster{ ID: strings.TrimSpace(cluster.ID), Name: cluster.Name, APIEndpoint: cluster.APIEndpoint, TLSFingerprint: cluster.TLSFingerprint, EncryptedToken: encryptedToken, TokenID: cluster.TokenID, Status: status, }) if err != nil { return Cluster{}, err } return Cluster{ ID: stored.ID, Name: stored.Name, APIEndpoint: stored.APIEndpoint, TLSFingerprint: stored.TLSFingerprint, TokenID: stored.TokenID, TokenSecret: cluster.TokenSecret, Status: stored.Status, CreatedAt: stored.CreatedAt, }, nil } func (r Repository) SetClusterStatus(ctx context.Context, id string, status string) (Cluster, bool, error) { stored, found, err := r.storage.SetStatus(ctx, strings.TrimSpace(id), strings.TrimSpace(status)) if err != nil || !found { return Cluster{}, found, err } return Cluster{ ID: stored.ID, Name: stored.Name, APIEndpoint: stored.APIEndpoint, TLSFingerprint: stored.TLSFingerprint, TokenID: stored.TokenID, Status: stored.Status, CreatedAt: stored.CreatedAt, }, true, nil } type SQLStorage struct { db *sql.DB } func (s SQLStorage) Get(ctx context.Context, id string) (StoredCluster, bool, error) { var cluster StoredCluster err := s.db.QueryRowContext(ctx, ` select id::text, name, api_endpoint, tls_fingerprint, encrypted_token, token_id, status, created_at from public.clusters where id = $1 `, id).Scan( &cluster.ID, &cluster.Name, &cluster.APIEndpoint, &cluster.TLSFingerprint, &cluster.EncryptedToken, &cluster.TokenID, &cluster.Status, &cluster.CreatedAt, ) if errors.Is(err, sql.ErrNoRows) { return StoredCluster{}, false, nil } if err != nil { return StoredCluster{}, false, err } return cluster, true, nil } func (s SQLStorage) Upsert(ctx context.Context, cluster StoredCluster) (StoredCluster, error) { var stored StoredCluster err := s.db.QueryRowContext(ctx, ` insert into public.clusters ( id, name, api_endpoint, tls_fingerprint, encrypted_token, token_id, status ) values ( coalesce(nullif($1, '')::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7 ) on conflict (id) do update set name = excluded.name, api_endpoint = excluded.api_endpoint, tls_fingerprint = excluded.tls_fingerprint, encrypted_token = excluded.encrypted_token, token_id = excluded.token_id, status = excluded.status returning id::text, name, api_endpoint, tls_fingerprint, encrypted_token, token_id, status, created_at `, cluster.ID, cluster.Name, cluster.APIEndpoint, cluster.TLSFingerprint, cluster.EncryptedToken, cluster.TokenID, cluster.Status, ).Scan( &stored.ID, &stored.Name, &stored.APIEndpoint, &stored.TLSFingerprint, &stored.EncryptedToken, &stored.TokenID, &stored.Status, &stored.CreatedAt, ) if err != nil { return StoredCluster{}, err } return stored, nil } func (s SQLStorage) SetStatus(ctx context.Context, id string, status string) (StoredCluster, bool, error) { var stored StoredCluster err := s.db.QueryRowContext(ctx, ` update public.clusters set status = $2 where id = $1 returning id::text, name, api_endpoint, tls_fingerprint, encrypted_token, token_id, status, created_at `, id, status).Scan( &stored.ID, &stored.Name, &stored.APIEndpoint, &stored.TLSFingerprint, &stored.EncryptedToken, &stored.TokenID, &stored.Status, &stored.CreatedAt, ) if errors.Is(err, sql.ErrNoRows) { return StoredCluster{}, false, nil } if err != nil { return StoredCluster{}, false, err } return stored, true, nil }