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}