Antonio Mika
·
2026-10-10
1package postgres
2
3import (
4 "errors"
5 "fmt"
6 "slices"
7 "strings"
8
9 "github.com/picosh/pico/pkg/db"
10)
11
12func (me *PsqlDB) shouldBlockSingup(ip string) error {
13 blocked := &db.BlockSignups{}
14 err := me.Db.Get(blocked, `SELECT * FROM block_signups WHERE ip = $1`, ip)
15 // an error in this case means the result is empty (no record found)
16 if err != nil {
17 return nil
18 }
19 return fmt.Errorf("your IP address has been blocked: %s", blocked.Reason)
20}
21
22func (me *PsqlDB) RegisterUser(username, pubkey, comment, ip string) (*db.User, error) {
23 lowerName := strings.ToLower(username)
24 valid, err := me.validateName(lowerName)
25 if !valid {
26 return nil, err
27 }
28
29 me.Logger.Info("checking if ip is in block list", "ip", ip, "username", username)
30 err = me.shouldBlockSingup(ip)
31 if err != nil {
32 me.Logger.Warn("user has been blocked from signing up", "ip", ip, "username", username, "err", err)
33 return nil, err
34 }
35
36 tx, err := me.Db.Beginx()
37 if err != nil {
38 return nil, err
39 }
40 defer func() {
41 _ = tx.Rollback()
42 }()
43
44 var id string
45 err = tx.QueryRow(`INSERT INTO app_users (name) VALUES($1) returning id`, lowerName).Scan(&id)
46 if err != nil {
47 return nil, err
48 }
49
50 err = me.insertPublicKeyWithTx(id, pubkey, comment, tx)
51 if err != nil {
52 return nil, err
53 }
54
55 err = tx.Commit()
56 if err != nil {
57 return nil, err
58 }
59
60 return me.FindUserByKey(username, pubkey)
61}
62
63func (me *PsqlDB) FindUserByKey(username string, key string) (*db.User, error) {
64 me.Logger.Info("attempting to find user with only public key", "key", key)
65 pk, err := me.findPublicKeyByKey(key)
66 if err == nil {
67 me.Logger.Info("found pubkey, looking for user", "key", key, "userId", pk.UserID)
68 user, err := me.FindUser(pk.UserID)
69 if err != nil {
70 return nil, err
71 }
72 user.PublicKey = pk
73 return user, nil
74 }
75
76 if errors.Is(err, &db.ErrMultiplePublicKeys{}) {
77 me.Logger.Info("detected multiple users with same public key", "user", username)
78 user, err := me.findUserForNameAndKey(username, key)
79 if err != nil {
80 me.Logger.Info("could not find user by username and public key", "user", username, "key", key)
81 // this is a little hacky but if we cannot find a user by name and public key
82 // then we return the multiple keys detected error so the user knows to specify their
83 // when logging in
84 return nil, &db.ErrMultiplePublicKeys{}
85 }
86 return user, nil
87 }
88
89 return nil, err
90}
91
92func (me *PsqlDB) FindUserByPubkey(key string) (*db.User, error) {
93 me.Logger.Info("attempting to find user with only public key", "key", key)
94 pk, err := me.findPublicKeyByKey(key)
95 if err != nil {
96 return nil, err
97 }
98
99 me.Logger.Info("found pubkey, looking for user", "key", key, "userId", pk.UserID)
100 user, err := me.FindUser(pk.UserID)
101 if err != nil {
102 return nil, err
103 }
104 user.PublicKey = pk
105 return user, nil
106}
107
108func (me *PsqlDB) FindUser(userID string) (*db.User, error) {
109 user := &db.User{}
110 err := me.Db.Get(user, `SELECT id, COALESCE(name, '') as name, created_at FROM app_users WHERE id = $1`, userID)
111 if err != nil {
112 return nil, err
113 }
114 return user, nil
115}
116
117func (me *PsqlDB) validateName(name string) (bool, error) {
118 lower := strings.ToLower(name)
119 if slices.Contains(db.DenyList, lower) {
120 return false, fmt.Errorf("%s is on deny list: %w", lower, db.ErrNameDenied)
121 }
122 v := db.NameValidator.MatchString(lower)
123 if !v {
124 return false, fmt.Errorf("%s is invalid: %w", lower, db.ErrNameInvalid)
125 }
126 user, _ := me.FindUserByName(lower)
127 if user == nil {
128 return true, nil
129 }
130 return false, fmt.Errorf("%s already taken: %w", lower, db.ErrNameTaken)
131}
132
133func (me *PsqlDB) FindUserByName(name string) (*db.User, error) {
134 user := &db.User{}
135 err := me.Db.Get(user, `SELECT * FROM app_users WHERE name = $1`, strings.ToLower(name))
136 if err != nil {
137 return nil, err
138 }
139 return user, nil
140}
141
142func (me *PsqlDB) findUserForNameAndKey(name string, key string) (*db.User, error) {
143 user := &db.User{}
144 pk := &db.PublicKey{}
145
146 r := me.Db.QueryRow(`SELECT app_users.id, app_users.name, app_users.created_at, public_keys.id as pk_id, public_keys.public_key, public_keys.created_at as pk_created_at FROM app_users LEFT JOIN public_keys ON public_keys.user_id = app_users.id WHERE app_users.name = $1 AND public_keys.public_key = $2`, strings.ToLower(name), key)
147 err := r.Scan(&user.ID, &user.Name, &user.CreatedAt, &pk.ID, &pk.Key, &pk.CreatedAt)
148 if err != nil {
149 return nil, err
150 }
151
152 user.PublicKey = pk
153 return user, nil
154}
155
156func (me *PsqlDB) FindUsers() ([]*db.User, error) {
157 var users []*db.User
158 err := me.Db.Select(&users, `SELECT id, COALESCE(name, '') as name, created_at FROM app_users ORDER BY name ASC`)
159 if err != nil {
160 return nil, err
161 }
162 return users, nil
163}
164
165func (me *PsqlDB) FindUserStats(userID string) (*db.UserStats, error) {
166 stats := db.UserStats{}
167 rs, err := me.Db.Queryx(`SELECT cur_space, count(id), min(created_at), max(created_at), max(updated_at) FROM posts WHERE user_id=$1 GROUP BY cur_space`, userID)
168 if err != nil {
169 return nil, err
170 }
171 defer func() { _ = rs.Close() }()
172
173 for rs.Next() {
174 stat := db.UserServiceStats{}
175 err := rs.Scan(&stat.Service, &stat.Num, &stat.FirstCreatedAt, &stat.LastestCreatedAt, &stat.LatestUpdatedAt)
176 if err != nil {
177 return nil, err
178 }
179 switch stat.Service {
180 case "prose":
181 stats.Prose = stat
182 case "pastes":
183 stats.Pastes = stat
184 case "feeds":
185 stats.Feeds = stat
186 }
187 }
188
189 if rs.Err() != nil {
190 return nil, rs.Err()
191 }
192
193 pgs, err := me.findPagesStats(userID)
194 if err != nil {
195 return nil, err
196 }
197 stats.Pages = *pgs
198 return &stats, err
199}