package db
import (
"database/sql"
"errors"
"fmt"
)
// Token is a row in the tokens table. The plaintext is never stored.
type Token struct {
ID int64
UserID int64
Name string
Hint string
CreatedAt int64
LastUsedAt sql.NullInt64
}
const tokenColumns = `SELECT id, user_id, name, hint, created_at, last_used_at FROM tokens WHERE `
// CreateToken stores a git token (tokenHash is a digest; hint a display
// prefix) and returns the stored row.
func CreateToken(database *sql.DB, userID int64, name, hint, tokenHash string) (Token, error) {
id, err := execLastID(database,
`INSERT INTO tokens (user_id, name, hint, token_hash) VALUES (?, ?, ?, ?)`,
userID, name, hint, tokenHash)
if err != nil {
return Token{}, fmt.Errorf("create token: %w", err)
}
return getToken(database, tokenColumns+`id = ?`, id)
}
// GetTokenByHash looks up a token by its digest; ErrNotFound when absent.
func GetTokenByHash(database *sql.DB, tokenHash string) (Token, error) {
return getToken(database, tokenColumns+`token_hash = ?`, tokenHash)
}
// scanToken reads one tokens row (see tokenColumns) into tok.
func scanToken(s rowScanner, tok *Token) error {
return s.Scan(&tok.ID, &tok.UserID, &tok.Name, &tok.Hint, &tok.CreatedAt, &tok.LastUsedAt)
}
func getToken(database *sql.DB, query string, arg any) (Token, error) {
var tok Token
err := scanToken(database.QueryRow(query, arg), &tok)
if errors.Is(err, sql.ErrNoRows) {
return Token{}, fmt.Errorf("token: %w", ErrNotFound)
}
if err != nil {
return Token{}, fmt.Errorf("get token: %w", err)
}
return tok, nil
}
// ListTokens returns a user's tokens, newest first.
func ListTokens(database *sql.DB, userID int64) ([]Token, error) {
tokens, err := listQuery(database,
tokenColumns+`user_id = ? ORDER BY created_at DESC, id DESC`, []any{userID}, scanToken)
if err != nil {
return nil, fmt.Errorf("list tokens: %w", err)
}
return tokens, nil
}
// TouchToken records that a token was just used.
func TouchToken(database *sql.DB, id int64) error {
if _, err := database.Exec(`UPDATE tokens SET last_used_at = unixepoch() WHERE id = ?`, id); err != nil {
return fmt.Errorf("touch token %d: %w", id, err)
}
return nil
}
// DeleteToken revokes a token, scoped to its owner.
func DeleteToken(database *sql.DB, userID, id int64) error {
return execScoped(database, fmt.Sprintf("delete token %d", id),
`DELETE FROM tokens WHERE id = ? AND user_id = ?`, id, userID)
}