josie / simplegit

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)
}