CI / changes (push) Successful in 7s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 28s
CI / go (push) Failing after 24s
CI / bird2 (push) Has been skipped
CI / release (push) Has been skipped
- Added endpoints for managing API keys, including creation, retrieval, updating, and revocation. - Introduced a new Auth session endpoint to retrieve current tenant and role information. - Updated the authentication middleware to support API key-based authentication and track last used timestamps. - Enhanced documentation to reflect new API key functionalities and usage guidelines. - Improved logging for demo authentication scenarios.
204 lines
5.4 KiB
Go
204 lines
5.4 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"evobgp/internal/authkey"
|
|
"evobgp/internal/store"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
func (p *Postgres) ListAPIKeys(tenantID string) ([]*store.APIKey, error) {
|
|
ctx := context.Background()
|
|
rows, err := p.pool.Query(ctx, `
|
|
SELECT id::text, name, role, token_prefix, created_at, updated_at, expires_at, revoked_at, last_used_at
|
|
FROM api_key WHERE tenant_id=$1 ORDER BY created_at DESC`, tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []*store.APIKey
|
|
for rows.Next() {
|
|
k, err := scanAPIKeyRow(rows.Scan, tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, k)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (p *Postgres) GetAPIKey(tenantID, id string) (*store.APIKey, error) {
|
|
ctx := context.Background()
|
|
row := p.pool.QueryRow(ctx, `
|
|
SELECT id::text, name, role, token_prefix, created_at, updated_at, expires_at, revoked_at, last_used_at
|
|
FROM api_key WHERE id=$1 AND tenant_id=$2`, id, tenantID)
|
|
k, err := scanAPIKeyRow(row.Scan, tenantID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, store.ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return k, nil
|
|
}
|
|
|
|
func (p *Postgres) CreateAPIKey(tenantID string, in *store.APIKeyCreate) (*store.APIKeyWithSecret, error) {
|
|
if in == nil || strings.TrimSpace(in.Name) == "" || !store.ValidAPIKeyRole(in.Role) {
|
|
return nil, store.ErrInvalidInput
|
|
}
|
|
tok, err := authkey.GenerateToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
id := uuid.NewString()
|
|
hash := authkey.HashToken(tok)
|
|
prefix := authkey.Prefix(tok)
|
|
role := strings.ToLower(strings.TrimSpace(in.Role))
|
|
ctx := context.Background()
|
|
_, err = p.pool.Exec(ctx, `
|
|
INSERT INTO api_key (id, tenant_id, name, role, token_prefix, token_hash, expires_at)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
|
|
id, tenantID, strings.TrimSpace(in.Name), role, prefix, hash, in.ExpiresAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
k, err := p.GetAPIKey(tenantID, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &store.APIKeyWithSecret{APIKey: *k, Token: tok}, nil
|
|
}
|
|
|
|
func (p *Postgres) UpdateAPIKey(tenantID, id string, patch *store.APIKeyPatch) (*store.APIKey, error) {
|
|
cur, err := p.GetAPIKey(tenantID, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cur.RevokedAt != nil {
|
|
return nil, store.ErrInvalidInput
|
|
}
|
|
if patch == nil {
|
|
return nil, store.ErrInvalidInput
|
|
}
|
|
if patch.Name != nil {
|
|
n := strings.TrimSpace(*patch.Name)
|
|
if n == "" {
|
|
return nil, store.ErrInvalidInput
|
|
}
|
|
cur.Name = n
|
|
}
|
|
if patch.Role != nil {
|
|
if !store.ValidAPIKeyRole(*patch.Role) {
|
|
return nil, store.ErrInvalidInput
|
|
}
|
|
cur.Role = strings.ToLower(strings.TrimSpace(*patch.Role))
|
|
}
|
|
if patch.ClearExpiresAt {
|
|
cur.ExpiresAt = nil
|
|
} else if patch.ExpiresAt != nil {
|
|
cur.ExpiresAt = patch.ExpiresAt
|
|
}
|
|
ctx := context.Background()
|
|
_, err = p.pool.Exec(ctx, `
|
|
UPDATE api_key SET name=$3, role=$4, expires_at=$5, updated_at=now()
|
|
WHERE id=$1 AND tenant_id=$2 AND revoked_at IS NULL`,
|
|
id, tenantID, cur.Name, cur.Role, cur.ExpiresAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.GetAPIKey(tenantID, id)
|
|
}
|
|
|
|
func (p *Postgres) RevokeAPIKey(tenantID, id string) error {
|
|
ctx := context.Background()
|
|
tag, err := p.pool.Exec(ctx, `
|
|
UPDATE api_key SET revoked_at=now(), updated_at=now()
|
|
WHERE id=$1 AND tenant_id=$2 AND revoked_at IS NULL`, id, tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return store.ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Postgres) RotateAPIKey(tenantID, id string) (*store.APIKeyWithSecret, error) {
|
|
cur, err := p.GetAPIKey(tenantID, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cur.RevokedAt != nil {
|
|
return nil, store.ErrInvalidInput
|
|
}
|
|
tok, err := authkey.GenerateToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hash := authkey.HashToken(tok)
|
|
prefix := authkey.Prefix(tok)
|
|
ctx := context.Background()
|
|
_, err = p.pool.Exec(ctx, `
|
|
UPDATE api_key SET token_hash=$3, token_prefix=$4, updated_at=now()
|
|
WHERE id=$1 AND tenant_id=$2 AND revoked_at IS NULL`,
|
|
id, tenantID, hash, prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
k, err := p.GetAPIKey(tenantID, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &store.APIKeyWithSecret{APIKey: *k, Token: tok}, nil
|
|
}
|
|
|
|
func (p *Postgres) ListActiveAPIKeyHashes() ([]store.APIKeyAuthRow, error) {
|
|
ctx := context.Background()
|
|
rows, err := p.pool.Query(ctx, `
|
|
SELECT id::text, tenant_id::text, role, token_hash
|
|
FROM api_key
|
|
WHERE revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []store.APIKeyAuthRow
|
|
for rows.Next() {
|
|
var row store.APIKeyAuthRow
|
|
var hash []byte
|
|
if err := rows.Scan(&row.ID, &row.TenantID, &row.Role, &hash); err != nil {
|
|
return nil, err
|
|
}
|
|
row.TokenHash = append([]byte(nil), hash...)
|
|
out = append(out, row)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (p *Postgres) TouchAPIKeyLastUsed(id string) error {
|
|
ctx := context.Background()
|
|
_, err := p.pool.Exec(ctx, `UPDATE api_key SET last_used_at=now() WHERE id=$1`, id)
|
|
return err
|
|
}
|
|
|
|
type scanFn func(dest ...any) error
|
|
|
|
func scanAPIKeyRow(scan scanFn, tenantID string) (*store.APIKey, error) {
|
|
var k store.APIKey
|
|
k.TenantID = tenantID
|
|
var expires, revoked, lastUsed *time.Time
|
|
if err := scan(&k.ID, &k.Name, &k.Role, &k.Prefix, &k.CreatedAt, &k.UpdatedAt, &expires, &revoked, &lastUsed); err != nil {
|
|
return nil, err
|
|
}
|
|
k.ExpiresAt = expires
|
|
k.RevokedAt = revoked
|
|
k.LastUsedAt = lastUsed
|
|
return &k, nil
|
|
}
|