This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/store/postgres/api_tokens.go
Jeffrey Smith f32eefab14 Feat v0.7.7 API tokens + extension permissions (#61)
Personal access tokens (PATs) for programmatic API access with
SHA-256 hashing, permission scoping (git model), and Settings/Admin UI.
Extension-declared user permissions with dynamic registry, gate_permission
manifest field, permissions Starlark module, and grouped admin UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:20:16 +00:00

126 lines
3.4 KiB
Go

package postgres
import (
"context"
"database/sql"
"encoding/json"
"time"
"armature/models"
)
// APITokenStore implements store.APITokenStore for PostgreSQL.
type APITokenStore struct {
db *sql.DB
}
// NewAPITokenStore creates a new PostgreSQL API token store.
func NewAPITokenStore(db *sql.DB) *APITokenStore {
return &APITokenStore{db: db}
}
func (s *APITokenStore) Create(ctx context.Context, token *models.APIToken) error {
permsJSON, err := json.Marshal(token.Permissions)
if err != nil {
return err
}
return s.db.QueryRowContext(ctx,
`INSERT INTO api_tokens (user_id, name, token_hash, prefix, permissions, expires_at, created_by)
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7)
RETURNING id, created_at`,
token.UserID, token.Name, token.TokenHash, token.Prefix,
string(permsJSON), token.ExpiresAt, token.CreatedBy,
).Scan(&token.ID, &token.CreatedAt)
}
func (s *APITokenStore) GetByHash(ctx context.Context, tokenHash string) (*models.APIToken, error) {
row := s.db.QueryRowContext(ctx,
`SELECT id, user_id, name, token_hash, prefix, permissions, expires_at, last_used_at, created_at, created_by
FROM api_tokens
WHERE token_hash = $1 AND (expires_at IS NULL OR expires_at > NOW())`,
tokenHash)
var t models.APIToken
var permsJSON []byte
err := row.Scan(&t.ID, &t.UserID, &t.Name, &t.TokenHash, &t.Prefix,
&permsJSON, &t.ExpiresAt, &t.LastUsedAt, &t.CreatedAt, &t.CreatedBy)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
if err := json.Unmarshal(permsJSON, &t.Permissions); err != nil {
t.Permissions = []string{}
}
return &t, nil
}
func (s *APITokenStore) ListForUser(ctx context.Context, userID string) ([]models.APIToken, error) {
rows, err := s.db.QueryContext(ctx,
`SELECT id, user_id, name, prefix, permissions, expires_at, last_used_at, created_at, created_by
FROM api_tokens
WHERE user_id = $1
ORDER BY created_at DESC`,
userID)
if err != nil {
return nil, err
}
defer rows.Close()
var tokens []models.APIToken
for rows.Next() {
var t models.APIToken
var permsJSON []byte
if err := rows.Scan(&t.ID, &t.UserID, &t.Name, &t.Prefix,
&permsJSON, &t.ExpiresAt, &t.LastUsedAt, &t.CreatedAt, &t.CreatedBy); err != nil {
return nil, err
}
if err := json.Unmarshal(permsJSON, &t.Permissions); err != nil {
t.Permissions = []string{}
}
tokens = append(tokens, t)
}
if tokens == nil {
tokens = []models.APIToken{}
}
return tokens, nil
}
func (s *APITokenStore) Revoke(ctx context.Context, id, userID string) (int64, error) {
res, err := s.db.ExecContext(ctx,
`DELETE FROM api_tokens WHERE id = $1 AND user_id = $2`,
id, userID)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (s *APITokenStore) RevokeByID(ctx context.Context, id string) (int64, error) {
res, err := s.db.ExecContext(ctx,
`DELETE FROM api_tokens WHERE id = $1`, id)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (s *APITokenStore) CleanExpired(ctx context.Context) (int64, error) {
res, err := s.db.ExecContext(ctx,
`DELETE FROM api_tokens WHERE expires_at IS NOT NULL AND expires_at < NOW()`)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (s *APITokenStore) UpdateLastUsed(ctx context.Context, id string) error {
_, err := s.db.ExecContext(ctx,
`UPDATE api_tokens SET last_used_at = $1 WHERE id = $2`,
time.Now(), id)
return err
}