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	"modernc.org/sqlite"
 12	sqlite3 "modernc.org/sqlite/lib"
 13)
 14
 15// Close closes the database.
 16func (d *SqliteBackend) Close() error {
 17	return d.db.Close()
 18}
 19
 20// init creates the database.
 21func (d *SqliteBackend) init() error {
 22	return wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
 23		if _, err := tx.Exec(sqlCreateSettingsTable); err != nil {
 24			return err
 25		}
 26		if _, err := tx.Exec(sqlCreateUserTable); err != nil {
 27			return err
 28		}
 29		if _, err := tx.Exec(sqlCreatePublicKeyTable); err != nil {
 30			return err
 31		}
 32		if _, err := tx.Exec(sqlCreateRepoTable); err != nil {
 33			return err
 34		}
 35		if _, err := tx.Exec(sqlCreateCollabTable); err != nil {
 36			return err
 37		}
 38
 39		// Set default settings.
 40		if _, err := tx.Exec("INSERT OR IGNORE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", "allow_keyless", true); err != nil {
 41			return err
 42		}
 43		if _, err := tx.Exec("INSERT OR IGNORE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", "anon_access", backend.ReadOnlyAccess.String()); err != nil {
 44			return err
 45		}
 46
 47		var init bool
 48		if err := tx.Get(&init, "SELECT value FROM settings WHERE key = 'init'"); err != nil && !errors.Is(err, sql.ErrNoRows) {
 49			return err
 50		}
 51
 52		// Create default user.
 53		if !init {
 54			r, err := tx.Exec("INSERT OR IGNORE INTO user (username, admin, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP);", "admin", true)
 55			if err != nil {
 56				return err
 57			}
 58			userID, err := r.LastInsertId()
 59			if err != nil {
 60				return err
 61			}
 62
 63			// Add initial keys
 64			// Don't use cfg.AdminKeys since it also includes the internal key
 65			// used for internal api access.
 66			for _, k := range d.cfg.InitialAdminKeys {
 67				pk, _, err := backend.ParseAuthorizedKey(k)
 68				if err != nil {
 69					d.logger.Error("error parsing initial admin key, skipping", "key", k, "err", err)
 70					continue
 71				}
 72
 73				stmt, err := tx.Prepare(`INSERT INTO public_key (user_id, public_key, updated_at)
 74					VALUES (?, ?, CURRENT_TIMESTAMP);`)
 75				if err != nil {
 76					return err
 77				}
 78
 79				defer stmt.Close() // nolint: errcheck
 80				if _, err := stmt.Exec(userID, backend.MarshalAuthorizedKey(pk)); err != nil {
 81					return err
 82				}
 83			}
 84		}
 85
 86		// set init flag
 87		if _, err := tx.Exec("INSERT OR IGNORE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", "init", true); err != nil {
 88			return err
 89		}
 90
 91		return nil
 92	})
 93}
 94
 95func wrapDbErr(err error) error {
 96	if err != nil {
 97		if errors.Is(err, sql.ErrNoRows) {
 98			return ErrNoRecord
 99		}
100		if liteErr, ok := err.(*sqlite.Error); ok {
101			code := liteErr.Code()
102			if code == sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY ||
103				code == sqlite3.SQLITE_CONSTRAINT_UNIQUE {
104				return ErrDuplicateKey
105			}
106		}
107	}
108	return err
109}
110
111func wrapTx(db *sqlx.DB, ctx context.Context, fn func(tx *sqlx.Tx) error) error {
112	tx, err := db.BeginTxx(ctx, nil)
113	if err != nil {
114		return fmt.Errorf("failed to begin transaction: %w", err)
115	}
116
117	if err := fn(tx); err != nil {
118		return rollback(tx, err)
119	}
120
121	if err := tx.Commit(); err != nil {
122		if errors.Is(err, sql.ErrTxDone) {
123			// this is ok because whoever did finish the tx should have also written the error already.
124			return nil
125		}
126		return fmt.Errorf("failed to commit transaction: %w", err)
127	}
128
129	return nil
130}
131
132func rollback(tx *sqlx.Tx, err error) error {
133	if rerr := tx.Rollback(); rerr != nil {
134		if errors.Is(rerr, sql.ErrTxDone) {
135			return err
136		}
137		return fmt.Errorf("failed to rollback: %s: %w", err.Error(), rerr)
138	}
139
140	return err
141}