1package sqlite
  2
  3import (
  4	"context"
  5	"database/sql"
  6	"errors"
  7	"fmt"
  8
  9	"github.com/charmbracelet/soft-serve/server/backend"
 10	"github.com/jmoiron/sqlx"
 11	"golang.org/x/crypto/bcrypt"
 12	"modernc.org/sqlite"
 13	sqlite3 "modernc.org/sqlite/lib"
 14)
 15
 16// Close closes the database.
 17func (d *SqliteBackend) Close() error {
 18	return d.db.Close()
 19}
 20
 21// init creates the database.
 22func (d *SqliteBackend) init() error {
 23	return wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
 24		if _, err := tx.Exec(sqlCreateSettingsTable); err != nil {
 25			return err
 26		}
 27		if _, err := tx.Exec(sqlCreateUserTable); err != nil {
 28			return err
 29		}
 30		if _, err := tx.Exec(sqlCreatePublicKeyTable); err != nil {
 31			return err
 32		}
 33		if _, err := tx.Exec(sqlCreateRepoTable); err != nil {
 34			return err
 35		}
 36		if _, err := tx.Exec(sqlCreateCollabTable); err != nil {
 37			return err
 38		}
 39
 40		// Set default settings.
 41		if _, err := tx.Exec("INSERT OR IGNORE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", "allow_keyless", true); err != nil {
 42			return err
 43		}
 44		if _, err := tx.Exec("INSERT OR IGNORE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", "anon_access", backend.ReadOnlyAccess.String()); err != nil {
 45			return err
 46		}
 47
 48		return nil
 49	})
 50}
 51
 52func wrapDbErr(err error) error {
 53	if err != nil {
 54		if errors.Is(err, sql.ErrNoRows) {
 55			return ErrNoRecord
 56		}
 57		if liteErr, ok := err.(*sqlite.Error); ok {
 58			code := liteErr.Code()
 59			if code == sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY ||
 60				code == sqlite3.SQLITE_CONSTRAINT_UNIQUE {
 61				return ErrDuplicateKey
 62			}
 63		}
 64	}
 65	return err
 66}
 67
 68func wrapTx(db *sqlx.DB, ctx context.Context, fn func(tx *sqlx.Tx) error) error {
 69	tx, err := db.BeginTxx(ctx, nil)
 70	if err != nil {
 71		return fmt.Errorf("failed to begin transaction: %w", err)
 72	}
 73
 74	if err := fn(tx); err != nil {
 75		return rollback(tx, err)
 76	}
 77
 78	if err := tx.Commit(); err != nil {
 79		if errors.Is(err, sql.ErrTxDone) {
 80			// this is ok because whoever did finish the tx should have also written the error already.
 81			return nil
 82		}
 83		return fmt.Errorf("failed to commit transaction: %w", err)
 84	}
 85
 86	return nil
 87}
 88
 89func rollback(tx *sqlx.Tx, err error) error {
 90	if rerr := tx.Rollback(); rerr != nil {
 91		if errors.Is(rerr, sql.ErrTxDone) {
 92			return err
 93		}
 94		return fmt.Errorf("failed to rollback: %s: %w", err.Error(), rerr)
 95	}
 96
 97	return err
 98}
 99
100func hashPassword(password string) (string, error) {
101	hash, err := bcrypt.GenerateFromPassword([]byte(password+"soft-serve-v1"), 14)
102	if err != nil {
103		return "", fmt.Errorf("failed to hash password: %w", err)
104	}
105
106	return string(hash), nil
107}
108
109func checkPassword(hash, password string) error {
110	if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+"soft-serve-v1")); err != nil {
111		return fmt.Errorf("failed to check password: %w", err)
112	}
113
114	return nil
115}