1package migrate
  2
  3import (
  4	"context"
  5	"errors"
  6	"fmt"
  7
  8	"github.com/charmbracelet/soft-serve/server/access"
  9	"github.com/charmbracelet/soft-serve/server/config"
 10	"github.com/charmbracelet/soft-serve/server/db"
 11	"github.com/charmbracelet/soft-serve/server/sshutils"
 12)
 13
 14const (
 15	createTablesName    = "create tables"
 16	createTablesVersion = 1
 17)
 18
 19var createTables = Migration{
 20	Version: createTablesVersion,
 21	Name:    createTablesName,
 22	Migrate: func(ctx context.Context, tx *db.Tx) error {
 23		cfg := config.FromContext(ctx)
 24
 25		insert := "INSERT "
 26
 27		// Alter old tables (if exist)
 28		// This is to support prior versions of Soft Serve v0.6
 29		switch tx.DriverName() {
 30		case "sqlite3", "sqlite":
 31			insert += "OR IGNORE "
 32
 33			hasUserTable := hasTable(tx, "user")
 34			if hasUserTable {
 35				if _, err := tx.ExecContext(ctx, "ALTER TABLE user RENAME TO user_old"); err != nil {
 36					return err
 37				}
 38			}
 39
 40			if hasTable(tx, "public_key") {
 41				if _, err := tx.ExecContext(ctx, "ALTER TABLE public_key RENAME TO public_key_old"); err != nil {
 42					return err
 43				}
 44			}
 45
 46			if hasTable(tx, "collab") {
 47				if _, err := tx.ExecContext(ctx, "ALTER TABLE collab RENAME TO collab_old"); err != nil {
 48					return err
 49				}
 50			}
 51
 52			if hasTable(tx, "repo") {
 53				if _, err := tx.ExecContext(ctx, "ALTER TABLE repo RENAME TO repo_old"); err != nil {
 54					return err
 55				}
 56			}
 57		}
 58
 59		if err := migrateUp(ctx, tx, createTablesVersion, createTablesName); err != nil {
 60			return err
 61		}
 62
 63		switch tx.DriverName() {
 64		case "sqlite3", "sqlite":
 65
 66			if _, err := tx.ExecContext(ctx, "PRAGMA foreign_keys = OFF"); err != nil {
 67				return err
 68			}
 69
 70			if hasTable(tx, "user_old") {
 71				sqlm := `
 72				INSERT INTO users (id, username, admin, updated_at)
 73					SELECT id, username, admin, updated_at FROM user_old;
 74				`
 75				if _, err := tx.ExecContext(ctx, sqlm); err != nil {
 76					return err
 77				}
 78			}
 79
 80			if hasTable(tx, "public_key_old") {
 81				// Check duplicate keys
 82				pks := []struct {
 83					ID        string `db:"id"`
 84					PublicKey string `db:"public_key"`
 85				}{}
 86				if err := tx.SelectContext(ctx, &pks, "SELECT id, public_key FROM public_key_old"); err != nil {
 87					return err
 88				}
 89
 90				pkss := map[string]struct{}{}
 91				for _, pk := range pks {
 92					if _, ok := pkss[pk.PublicKey]; ok {
 93						return fmt.Errorf("duplicate public key: %q, please remove the duplicate key and try again", pk.PublicKey)
 94					}
 95					pkss[pk.PublicKey] = struct{}{}
 96				}
 97
 98				sqlm := `
 99				INSERT INTO public_keys (id, user_id, public_key, created_at, updated_at)
100					SELECT id, user_id, public_key, created_at, updated_at FROM public_key_old;
101				`
102				if _, err := tx.ExecContext(ctx, sqlm); err != nil {
103					return err
104				}
105			}
106
107			if hasTable(tx, "repo_old") {
108				sqlm := `
109				INSERT INTO repos (id, name, project_name, description, private,mirror, hidden, created_at, updated_at, user_id)
110					SELECT id, name, project_name, description, private, mirror, hidden, created_at, updated_at, (
111						SELECT id FROM users WHERE admin = true ORDER BY id LIMIT 1
112				) FROM repo_old;
113				`
114				if _, err := tx.ExecContext(ctx, sqlm); err != nil {
115					return err
116				}
117			}
118
119			if hasTable(tx, "collab_old") {
120				sqlm := `
121				INSERT INTO collabs (id, user_id, repo_id, created_at, updated_at)
122					SELECT id, user_id, repo_id, created_at, updated_at FROM collab_old;
123				`
124				if _, err := tx.ExecContext(ctx, sqlm); err != nil {
125					return err
126				}
127			}
128
129			if _, err := tx.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
130				return err
131			}
132		}
133
134		// Insert default user
135		insertUser := tx.Rebind(insert + "INTO users (username, admin, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)")
136		if _, err := tx.ExecContext(ctx, insertUser, "admin", true); err != nil {
137			return err
138		}
139
140		for _, k := range cfg.AdminKeys() {
141			query := insert + "INTO public_keys (user_id, public_key, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)"
142			if tx.DriverName() == "postgres" {
143				query += " ON CONFLICT DO NOTHING"
144			}
145
146			query = tx.Rebind(query)
147			ak := sshutils.MarshalAuthorizedKey(k)
148			if _, err := tx.ExecContext(ctx, query, 1, ak); err != nil {
149				if errors.Is(db.WrapError(err), db.ErrDuplicateKey) {
150					continue
151				}
152				return err
153			}
154		}
155
156		// Insert default settings
157		insertSettings := insert + "INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)"
158		insertSettings = tx.Rebind(insertSettings)
159		settings := []struct {
160			Key   string
161			Value string
162		}{
163			{"allow_keyless", "true"},
164			{"anon_access", access.ReadOnlyAccess.String()},
165			{"init", "true"},
166		}
167
168		for _, s := range settings {
169			if _, err := tx.ExecContext(ctx, insertSettings, s.Key, s.Value); err != nil {
170				return fmt.Errorf("inserting default settings %q: %w", s.Key, err)
171			}
172		}
173
174		return nil
175	},
176	Rollback: func(ctx context.Context, tx *db.Tx) error {
177		return migrateDown(ctx, tx, createTablesVersion, createTablesName)
178	},
179}