main pico / pkg / apps / pgs / db / postgres.go
Antonio Mika  ·  2026-10-25
  1package pgsdb
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"log/slog"
  7	"time"
  8
  9	"github.com/jmoiron/sqlx"
 10	_ "github.com/lib/pq"
 11	"github.com/picosh/pico/pkg/db"
 12	"github.com/picosh/pico/pkg/shared"
 13)
 14
 15type PgsPsqlDB struct {
 16	Logger *slog.Logger
 17	Db     *sqlx.DB
 18}
 19
 20var _ PgsDB = (*PgsPsqlDB)(nil)
 21
 22func NewDB(databaseUrl string, logger *slog.Logger) (*PgsPsqlDB, error) {
 23	var err error
 24	d := &PgsPsqlDB{
 25		Logger: logger,
 26	}
 27	d.Logger.Info("connecting to postgres", "databaseUrl", databaseUrl)
 28
 29	db, err := sqlx.Connect("postgres", databaseUrl)
 30	if err != nil {
 31		return nil, err
 32	}
 33
 34	d.Db = db
 35	return d, nil
 36}
 37
 38func NewDBWithConn(db *sqlx.DB, logger *slog.Logger) *PgsPsqlDB {
 39	return &PgsPsqlDB{
 40		Logger: logger,
 41		Db:     db,
 42	}
 43}
 44
 45func (me *PgsPsqlDB) Close() error {
 46	return me.Db.Close()
 47}
 48
 49func (me *PgsPsqlDB) FindUsers() ([]*db.User, error) {
 50	users := []*db.User{}
 51	err := me.Db.Select(&users, "SELECT * FROM app_users")
 52	return users, err
 53}
 54
 55func (me *PgsPsqlDB) FindUserByPubkey(key string) (*db.User, error) {
 56	pk := []db.PublicKey{}
 57	err := me.Db.Select(&pk, "SELECT * FROM public_keys WHERE public_key=$1", key)
 58	if err != nil {
 59		return nil, err
 60	}
 61	if len(pk) == 0 {
 62		return nil, fmt.Errorf("pubkey not found in our database: [%s]", key)
 63	}
 64	// When we run PublicKeyByKey and there are multiple public keys returned from the database
 65	// that should mean that we don't have the correct username for this public key.
 66	// When that happens we need to reject the authentication and ask the user to provide the correct
 67	// username when using ssh.  So instead of `ssh <domain>` it should be `ssh user@<domain>`
 68	if len(pk) > 1 {
 69		return nil, &db.ErrMultiplePublicKeys{}
 70	}
 71
 72	user, err := me.FindUser(pk[0].UserID)
 73	if err != nil {
 74		return nil, err
 75	}
 76
 77	user.PublicKey = &pk[0]
 78	return user, nil
 79}
 80
 81func (me *PgsPsqlDB) FindUser(userID string) (*db.User, error) {
 82	user := db.User{}
 83	err := me.Db.Get(&user, "SELECT * FROM app_users WHERE id=$1", userID)
 84	return &user, err
 85}
 86
 87func (me *PgsPsqlDB) FindUserByName(name string) (*db.User, error) {
 88	user := db.User{}
 89	err := me.Db.Get(&user, "SELECT * FROM app_users WHERE name=$1", name)
 90	return &user, err
 91}
 92
 93func (me *PgsPsqlDB) FindFeature(userID, name string) (*db.FeatureFlag, error) {
 94	ff := db.FeatureFlag{}
 95	err := me.Db.Get(&ff, "SELECT * FROM feature_flags WHERE user_id=$1 AND name=$2 ORDER BY expires_at DESC LIMIT 1", userID, name)
 96	return &ff, err
 97}
 98
 99func (me *PgsPsqlDB) InsertAccessLog(log *db.AccessLog) error {
100	_, err := me.Db.Exec(
101		`INSERT INTO access_logs (user_id, service, pubkey, identity) VALUES ($1, $2, $3, $4);`,
102		log.UserID,
103		log.Service,
104		log.Pubkey,
105		log.Identity,
106	)
107	return err
108}
109
110func (me *PgsPsqlDB) InsertProject(userID, name, projectDir string) (string, error) {
111	if !shared.IsValidSubdomain(name) {
112		return "", fmt.Errorf("'%s' is not a valid project name, must match /^[a-z0-9-]+$/", name)
113	}
114
115	var projectID string
116	row := me.Db.QueryRow(
117		"INSERT INTO projects (user_id, name, project_dir) VALUES ($1, $2, $3) RETURNING id",
118		userID,
119		name,
120		projectDir,
121	)
122	err := row.Scan(&projectID)
123	return projectID, err
124}
125
126func (me *PgsPsqlDB) UpdateProject(userID, name string) error {
127	_, err := me.Db.Exec("UPDATE projects SET updated_at=$1 WHERE user_id=$2 AND name=$3", time.Now(), userID, name)
128	return err
129}
130
131func (me *PgsPsqlDB) UpsertProject(userID, projectName, projectDir string) (*db.Project, error) {
132	project, err := me.FindProjectByName(userID, projectName)
133	if err == nil {
134		// this just updates the `createdAt` timestamp, useful for book-keeping
135		err = me.UpdateProject(userID, projectName)
136		if err != nil {
137			me.Logger.Error(
138				"could not update project",
139				"err", err,
140				"projectName", projectName,
141				"projectDir", projectDir,
142			)
143			return nil, err
144		}
145		return project, nil
146	}
147
148	_, err = me.InsertProject(userID, projectName, projectName)
149	if err != nil {
150		me.Logger.Error(
151			"could not create project",
152			"err", err,
153			"projectName", projectName,
154			"projectDir", projectDir,
155		)
156		return nil, err
157	}
158	if IsProjectPrivate(projectName) {
159		err = me.UpdateProjectAcl(userID, projectName, db.ProjectAcl{Type: "private", Data: []string{}})
160		if err != nil {
161			return nil, err
162		}
163	}
164	return me.FindProjectByName(userID, projectName)
165}
166
167func (me *PgsPsqlDB) LinkToProject(userID, projectID, projectDir string, commit bool) error {
168	linkToProject, err := me.FindProjectByName(userID, projectDir)
169	if err != nil {
170		return err
171	}
172	isAlreadyLinked := linkToProject.Name != linkToProject.ProjectDir
173	sameProject := linkToProject.ID == projectID
174
175	/*
176		A project linked to another project which is also linked to a
177		project is forbidden.  CI/CD Example:
178			- ProjectProd links to ProjectStaging
179			- ProjectStaging links to ProjectMain
180			- We merge `main` and trigger a deploy which uploads to ProjectMain
181			- All three get updated immediately
182		This scenario was not the intent of our CI/CD.  What we actually
183		wanted was to create a snapshot of ProjectMain and have ProjectStaging
184		link to the snapshot, but that's not the intended design of pgs.
185
186		So we want to close that gap here.
187
188		We ensure that `project.Name` and `project.ProjectDir` are identical
189		when there is no aliasing.
190	*/
191	if !sameProject && isAlreadyLinked {
192		return fmt.Errorf(
193			"cannot link (%s) to (%s) because it is also a link to (%s)",
194			projectID,
195			projectDir,
196			linkToProject.ProjectDir,
197		)
198	}
199
200	if commit {
201		_, err = me.Db.Exec(
202			"UPDATE projects SET project_dir=$1, updated_at=$2 WHERE id=$3",
203			projectDir,
204			time.Now(),
205			projectID,
206		)
207	}
208	return err
209}
210
211func (me *PgsPsqlDB) RemoveProject(projectID string) error {
212	_, err := me.Db.Exec("DELETE FROM projects WHERE id=$1", projectID)
213	return err
214}
215
216func (me *PgsPsqlDB) FindProjectByName(userID, name string) (*db.Project, error) {
217	project := db.Project{}
218	err := me.Db.Get(&project, "SELECT * FROM projects WHERE user_id=$1 AND name=$2", userID, name)
219	return &project, err
220}
221
222func (me *PgsPsqlDB) FindProjectLinks(userID, name string) ([]*db.Project, error) {
223	projects := []*db.Project{}
224	err := me.Db.Select(
225		&projects,
226		"SELECT * FROM projects WHERE user_id=$1 AND name != project_dir AND project_dir=$2 ORDER BY name ASC",
227		userID,
228		name,
229	)
230	return projects, err
231}
232
233func (me *PgsPsqlDB) FindProjectsByPrefix(userID, prefix string) ([]*db.Project, error) {
234	projects := []*db.Project{}
235	err := me.Db.Select(
236		&projects,
237		"SELECT * FROM projects WHERE user_id=$1 AND name=project_dir AND name ILIKE $2 ORDER BY updated_at ASC, name ASC",
238		userID,
239		prefix+"%",
240	)
241	return projects, err
242}
243
244func (me *PgsPsqlDB) FindProjectsByUser(userID string) ([]*db.Project, error) {
245	projects := []*db.Project{}
246	err := me.Db.Select(
247		&projects,
248		"SELECT * FROM projects WHERE user_id=$1 ORDER BY name ASC",
249		userID,
250	)
251	return projects, err
252}
253
254func (me *PgsPsqlDB) FindProjects(by string) ([]*db.Project, error) {
255	// A bind parameter in ORDER BY sorts by a constant, so map to a column.
256	orderBy := "p.created_at"
257	if by == "updated_at" {
258		orderBy = "p.updated_at"
259	}
260	projects := []*db.Project{}
261	err := me.Db.Select(
262		&projects,
263		fmt.Sprintf(`SELECT p.id, p.user_id, u.name as username, p.name, p.project_dir, p.acl, p.blocked, p.created_at, p.updated_at
264		FROM projects AS p
265		LEFT JOIN app_users AS u ON u.id = p.user_id
266		ORDER BY %s DESC`, orderBy),
267	)
268	return projects, err
269}
270
271func (me *PgsPsqlDB) UpdateProjectAcl(userID, name string, acl db.ProjectAcl) error {
272	_, err := me.Db.Exec(
273		"UPDATE projects SET acl=$3, updated_at=$4 WHERE user_id=$1 AND name=$2",
274		userID, name, acl, time.Now(),
275	)
276	return err
277}
278
279func (me *PgsPsqlDB) RegisterAdmin(username, pubkey, pubkeyName string) error {
280	if pubkeyName == "" {
281		pubkeyName = "main"
282	}
283	var userID string
284	row := me.Db.QueryRow("INSERT INTO app_users (name) VALUES ($1) RETURNING id", username)
285	err := row.Scan(&userID)
286	if err != nil {
287		return err
288	}
289
290	_, err = me.Db.Exec("INSERT INTO public_keys (user_id, name, public_key) VALUES ($1, $2, $3)", userID, pubkeyName, pubkey)
291	if err != nil {
292		return err
293	}
294
295	_, err = me.Db.Exec("INSERT INTO feature_flags (user_id, name, expires_at) VALUES (1, 'plus', '2100-01-01')", userID)
296	if err != nil {
297		return err
298	}
299
300	_, err = me.Db.Exec("INSERT INTO feature_flags (user_id, name, expires_at) VALUES (1, 'admin', '2100-01-01')", userID)
301	return err
302}
303
304func (me *PgsPsqlDB) InsertFormEntry(userID, name string, data map[string]interface{}) error {
305	dataJSON, err := json.Marshal(data)
306	if err != nil {
307		return err
308	}
309
310	_, err = me.Db.Exec(
311		"INSERT INTO form_entries (user_id, name, data) VALUES ($1, $2, $3)",
312		userID, name, dataJSON,
313	)
314	return err
315}
316
317func (me *PgsPsqlDB) FindFormEntriesByUserAndName(userID, name string) ([]*db.FormEntry, error) {
318	entries := []*db.FormEntry{}
319	err := me.Db.Select(
320		&entries,
321		"SELECT * FROM form_entries WHERE user_id=$1 AND name=$2 ORDER BY created_at DESC",
322		userID, name,
323	)
324	return entries, err
325}
326
327func (me *PgsPsqlDB) FindFormNamesByUser(userID string) ([]string, error) {
328	names := []string{}
329	err := me.Db.Select(
330		&names,
331		"SELECT DISTINCT name FROM form_entries WHERE user_id=$1 ORDER BY name ASC",
332		userID,
333	)
334	return names, err
335}
336
337func (me *PgsPsqlDB) RemoveFormEntriesByUserAndName(userID, name string) error {
338	_, err := me.Db.Exec("DELETE FROM form_entries WHERE user_id=$1 AND name=$2", userID, name)
339	return err
340}