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/sqlite/api_tokens.go
Jeffrey Smith e4f0bdbd36
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-runners (push) Has been skipped
CI/CD / test-go-pg (push) Successful in 2m48s
CI/CD / test-sqlite (push) Successful in 2m49s
CI/CD / build-and-deploy (push) Successful in 28s
Feat v0.7.7 api tokens ext permissions (#61)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-02 19:11:47 +00:00

173 lines
4.6 KiB
Go

package sqlite
import (
"context"
"database/sql"
"encoding/json"
"time"
"armature/models"
"github.com/google/uuid"
)
// APITokenStore implements store.APITokenStore for SQLite.
type APITokenStore struct{}
// NewAPITokenStore creates a new SQLite API token store.
func NewAPITokenStore() *APITokenStore {
return &APITokenStore{}
}
func (s *APITokenStore) Create(ctx context.Context, token *models.APIToken) error {
token.ID = uuid.New().String()
token.CreatedAt = time.Now().UTC()
permsJSON, err := json.Marshal(token.Permissions)
if err != nil {
return err
}
_, err = DB.ExecContext(ctx,
`INSERT INTO api_tokens (id, user_id, name, token_hash, prefix, permissions, expires_at, created_by, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
token.ID, token.UserID, token.Name, token.TokenHash, token.Prefix,
string(permsJSON), formatNullableTime(token.ExpiresAt), token.CreatedBy, token.CreatedAt.UTC().Format(time.RFC3339),
)
return err
}
func (s *APITokenStore) GetByHash(ctx context.Context, tokenHash string) (*models.APIToken, error) {
row := 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 = ? AND (expires_at IS NULL OR expires_at > datetime('now'))`,
tokenHash)
var t models.APIToken
var permsJSON string
var expiresAt, lastUsedAt, createdAt sql.NullString
var createdBy sql.NullString
err := row.Scan(&t.ID, &t.UserID, &t.Name, &t.TokenHash, &t.Prefix,
&permsJSON, &expiresAt, &lastUsedAt, &createdAt, &createdBy)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(permsJSON), &t.Permissions); err != nil {
t.Permissions = []string{}
}
t.ExpiresAt = parseNullableTime(expiresAt)
t.LastUsedAt = parseNullableTime(lastUsedAt)
if createdAt.Valid {
if ts, err := time.Parse(time.RFC3339, createdAt.String); err == nil {
t.CreatedAt = ts
}
}
if createdBy.Valid {
t.CreatedBy = &createdBy.String
}
return &t, nil
}
func (s *APITokenStore) ListForUser(ctx context.Context, userID string) ([]models.APIToken, error) {
rows, err := 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 = ?
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 string
var expiresAt, lastUsedAt, createdAt sql.NullString
var createdBy sql.NullString
if err := rows.Scan(&t.ID, &t.UserID, &t.Name, &t.Prefix,
&permsJSON, &expiresAt, &lastUsedAt, &createdAt, &createdBy); err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(permsJSON), &t.Permissions); err != nil {
t.Permissions = []string{}
}
t.ExpiresAt = parseNullableTime(expiresAt)
t.LastUsedAt = parseNullableTime(lastUsedAt)
if createdAt.Valid {
if ts, err := time.Parse(time.RFC3339, createdAt.String); err == nil {
t.CreatedAt = ts
}
}
if createdBy.Valid {
t.CreatedBy = &createdBy.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 := DB.ExecContext(ctx,
`DELETE FROM api_tokens WHERE id = ? AND user_id = ?`,
id, userID)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (s *APITokenStore) RevokeByID(ctx context.Context, id string) (int64, error) {
res, err := DB.ExecContext(ctx,
`DELETE FROM api_tokens WHERE id = ?`, id)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (s *APITokenStore) CleanExpired(ctx context.Context) (int64, error) {
res, err := DB.ExecContext(ctx,
`DELETE FROM api_tokens WHERE expires_at IS NOT NULL AND expires_at < datetime('now')`)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (s *APITokenStore) UpdateLastUsed(ctx context.Context, id string) error {
_, err := DB.ExecContext(ctx,
`UPDATE api_tokens SET last_used_at = ? WHERE id = ?`,
time.Now().UTC().Format(time.RFC3339), id)
return err
}
// ── Helpers ──────────────────────────────────
func formatNullableTime(t *time.Time) interface{} {
if t == nil {
return nil
}
return t.UTC().Format(time.RFC3339)
}
func parseNullableTime(ns sql.NullString) *time.Time {
if !ns.Valid || ns.String == "" {
return nil
}
t, err := time.Parse(time.RFC3339, ns.String)
if err != nil {
return nil
}
return &t
}