pico

created pr with pico/141.1 on 2026-09-26T01:18:41Z · by c8ef7d19
added pico/141.2 on 2026-09-26T01:40:03Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
-: ------- > 2: ee606d0 chore(prose): require plus or prose feature flag in order to upload
added pico/141.3 on 2026-09-26T14:41:34Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
2: ee606d0 = 2: ee606d0 chore(prose): require plus or prose feature flag in order to upload
-: ------- > 3: e14a99e refactor: remove unnecessary tests
added pico/141.4 on 2026-09-26T14:42:29Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
2: ee606d0 < -: ------- chore(prose): require plus or prose feature flag in order to upload
3: e14a99e ! 2: 7fb4f8f chore(prose): require plus or prose feature flag in order to upload
added pico/141.5 on 2026-09-26T15:11:30Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
2: 7fb4f8f ! 2: e469694 chore(prose): require plus or prose feature flag in order to upload
added pico/141.6 on 2026-10-01T13:39:16Z · by c8ef7d19
1: 2845ffb ! 1: 1e9cddc style(prose): minimal design ethos
2: e469694 ! 2: 0967882 chore(prose): remove with_styles and layout front-matter fields
cmds
checkout latest patchset:
ssh pr.pico.sh pull pico:141 | git am -3
checkout specific patchset revision:
ssh pr.pico.sh pull pico:141 [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pico:141
add comment to patch request:
ssh pr.pico.sh comment pico:141 "lgtm!"
+1 -1 pkg/apps/prose/ssh.go #
......@@ -57,7 +57,7 @@ func StartSshServer() {
5757 ".lxt": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
5858 "fallback": uploadimgs.NewUploadImgHandler(dbh, cfg, st),
5959 }
60- handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
60+ handler := NewUploadHandler(cfg, dbh, fileMap)
6161
6262 sshAuth := shared.NewSshAuthHandler(dbh, logger, "prose")
6363
+110 -0 pkg/apps/prose/uploader.go #
......@@ -0,0 +1,110 @@
1+package prose
2+
3+import (
4+ "fmt"
5+ "log/slog"
6+ "strings"
7+
8+ "github.com/picosh/pico/pkg/db"
9+ "github.com/picosh/pico/pkg/filehandlers"
10+ "github.com/picosh/pico/pkg/pssh"
11+ sendutils "github.com/picosh/pico/pkg/send/utils"
12+ "github.com/picosh/pico/pkg/shared"
13+)
14+
15+type ctxFeatureFlagKey struct{}
16+
17+func getFeatureFlag(s *pssh.SSHServerConnSession) *db.FeatureFlag {
18+ v := s.Context().Value(ctxFeatureFlagKey{})
19+ if v == nil {
20+ return nil
21+ }
22+ ff := s.Context().Value(ctxFeatureFlagKey{}).(*db.FeatureFlag)
23+ return ff
24+}
25+
26+func setFeatureFlag(s *pssh.SSHServerConnSession, ff *db.FeatureFlag) {
27+ s.SetValue(ctxFeatureFlagKey{}, ff)
28+}
29+
30+func setFeatureLimits(ff *db.FeatureFlag, cfg *shared.ConfigSite) {
31+ ff.Data.StorageMax = ff.FindStorageMax(cfg.MaxSize)
32+ ff.Data.FileMax = ff.FindFileMax(cfg.MaxAssetSize)
33+ ff.Data.SpecialFileMax = ff.FindSpecialFileMax(cfg.MaxSpecialFileSize)
34+}
35+
36+func findFeatureFlag(dbpool db.DB, cfg *shared.ConfigSite, userID string) (*db.FeatureFlag, error) {
37+ ff, err := dbpool.FindFeature(userID, "plus")
38+ if err == nil {
39+ if ff.IsValid() {
40+ setFeatureLimits(ff, cfg)
41+ return ff, nil
42+ }
43+ err = fmt.Errorf("ERROR: your pico+ has expired")
44+ }
45+
46+ ffProse, proseErr := dbpool.FindFeature(userID, "prose")
47+ if proseErr == nil {
48+ if ffProse.IsValid() {
49+ setFeatureLimits(ffProse, cfg)
50+ return ffProse, nil
51+ }
52+ proseErr = fmt.Errorf("ERROR: your prose access has expired")
53+ }
54+
55+ if err != nil && strings.Contains(err.Error(), "expired") {
56+ return nil, err
57+ }
58+ if proseErr != nil && strings.Contains(proseErr.Error(), "expired") {
59+ return nil, proseErr
60+ }
61+ return nil, fmt.Errorf("ERROR: uploading to prose requires the prose feature flag or pico+")
62+}
63+
64+type UploadHandler struct {
65+ *filehandlers.FileHandlerRouter
66+ Cfg *shared.ConfigSite
67+ DB db.DB
68+}
69+
70+var _ sendutils.CopyFromClientHandler = &UploadHandler{}
71+var _ sendutils.CopyFromClientHandler = (*UploadHandler)(nil)
72+
73+func NewUploadHandler(cfg *shared.ConfigSite, dbpool db.DB, fileMap map[string]filehandlers.ReadWriteHandler) *UploadHandler {
74+ router := filehandlers.NewFileHandlerRouter(cfg, dbpool, fileMap)
75+ return &UploadHandler{
76+ FileHandlerRouter: router,
77+ Cfg: cfg,
78+ DB: dbpool,
79+ }
80+}
81+
82+func (h *UploadHandler) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
83+ logger := pssh.GetLogger(s)
84+ if logger == nil {
85+ if h.Cfg != nil && h.Cfg.Logger != nil {
86+ return h.Cfg.Logger
87+ }
88+ return slog.Default()
89+ }
90+ return logger
91+}
92+
93+func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
94+ logger := h.GetLogger(s)
95+ user := pssh.GetUser(s)
96+
97+ if user == nil {
98+ err := fmt.Errorf("could not get user from ctx")
99+ logger.Error("error getting user from ctx", "err", err)
100+ return err
101+ }
102+
103+ ff, err := findFeatureFlag(h.DB, h.Cfg, user.ID)
104+ if err != nil {
105+ return err
106+ }
107+ setFeatureFlag(s, ff)
108+
109+ return h.FileHandlerRouter.Validate(s)
110+}
+227 -0 pkg/apps/prose/uploader_test.go #
......@@ -0,0 +1,227 @@
1+package prose
2+
3+import (
4+ "context"
5+ "fmt"
6+ "log/slog"
7+ "testing"
8+ "time"
9+
10+ "github.com/picosh/pico/pkg/db"
11+ "github.com/picosh/pico/pkg/db/stub"
12+ "github.com/picosh/pico/pkg/filehandlers"
13+ "github.com/picosh/pico/pkg/pssh"
14+ "github.com/picosh/pico/pkg/shared"
15+ "golang.org/x/crypto/ssh"
16+)
17+
18+type mockFeatureDB struct {
19+ *stub.StubDB
20+ features map[string]*db.FeatureFlag
21+}
22+
23+func newMockFeatureDB() *mockFeatureDB {
24+ return &mockFeatureDB{
25+ StubDB: stub.NewStubDB(slog.Default()),
26+ features: make(map[string]*db.FeatureFlag),
27+ }
28+}
29+
30+func (m *mockFeatureDB) FindFeature(userID, name string) (*db.FeatureFlag, error) {
31+ key := userID + ":" + name
32+ if ff, ok := m.features[key]; ok {
33+ return ff, nil
34+ }
35+ return nil, fmt.Errorf("feature flag %s not found for user %s", name, userID)
36+}
37+
38+func (m *mockFeatureDB) setFeature(userID string, ff *db.FeatureFlag) {
39+ key := userID + ":" + ff.Name
40+ m.features[key] = ff
41+}
42+
43+func TestFindFeatureFlag(t *testing.T) {
44+ cfg := NewConfigSite("prose-test")
45+ validExpires := time.Now().Add(24 * time.Hour)
46+ expiredExpires := time.Now().Add(-24 * time.Hour)
47+ userID := "user-1"
48+
49+ t.Run("plus valid returns plus", func(t *testing.T) {
50+ mockDB := newMockFeatureDB()
51+ plusFF := db.NewFeatureFlag(userID, "plus", uint64(50*shared.MB), int64(5*shared.MB), int64(2*shared.KB))
52+ plusFF.ExpiresAt = &validExpires
53+ mockDB.setFeature(userID, plusFF)
54+
55+ ff, err := findFeatureFlag(mockDB, cfg, userID)
56+ if err != nil {
57+ t.Fatalf("unexpected error: %v", err)
58+ }
59+ if ff.Name != "plus" {
60+ t.Errorf("expected plus, got %s", ff.Name)
61+ }
62+ })
63+
64+ t.Run("prose valid returns prose when no plus", func(t *testing.T) {
65+ mockDB := newMockFeatureDB()
66+ proseFF := db.NewFeatureFlag(userID, "prose", uint64(50*shared.MB), int64(5*shared.MB), int64(2*shared.KB))
67+ proseFF.ExpiresAt = &validExpires
68+ mockDB.setFeature(userID, proseFF)
69+
70+ ff, err := findFeatureFlag(mockDB, cfg, userID)
71+ if err != nil {
72+ t.Fatalf("unexpected error: %v", err)
73+ }
74+ if ff.Name != "prose" {
75+ t.Errorf("expected prose, got %s", ff.Name)
76+ }
77+ })
78+
79+ t.Run("plus valid takes precedence when both plus and prose exist", func(t *testing.T) {
80+ mockDB := newMockFeatureDB()
81+ plusFF := db.NewFeatureFlag(userID, "plus", uint64(50*shared.MB), int64(5*shared.MB), int64(2*shared.KB))
82+ plusFF.ExpiresAt = &validExpires
83+ mockDB.setFeature(userID, plusFF)
84+
85+ proseFF := db.NewFeatureFlag(userID, "prose", uint64(50*shared.MB), int64(5*shared.MB), int64(2*shared.KB))
86+ proseFF.ExpiresAt = &validExpires
87+ mockDB.setFeature(userID, proseFF)
88+
89+ ff, err := findFeatureFlag(mockDB, cfg, userID)
90+ if err != nil {
91+ t.Fatalf("unexpected error: %v", err)
92+ }
93+ if ff.Name != "plus" {
94+ t.Errorf("expected plus, got %s", ff.Name)
95+ }
96+ })
97+
98+ t.Run("plus expired falls back to prose if prose is valid", func(t *testing.T) {
99+ mockDB := newMockFeatureDB()
100+ plusFF := db.NewFeatureFlag(userID, "plus", uint64(50*shared.MB), int64(5*shared.MB), int64(2*shared.KB))
101+ plusFF.ExpiresAt = &expiredExpires
102+ mockDB.setFeature(userID, plusFF)
103+
104+ proseFF := db.NewFeatureFlag(userID, "prose", uint64(50*shared.MB), int64(5*shared.MB), int64(2*shared.KB))
105+ proseFF.ExpiresAt = &validExpires
106+ mockDB.setFeature(userID, proseFF)
107+
108+ ff, err := findFeatureFlag(mockDB, cfg, userID)
109+ if err != nil {
110+ t.Fatalf("unexpected error: %v", err)
111+ }
112+ if ff.Name != "prose" {
113+ t.Errorf("expected prose, got %s", ff.Name)
114+ }
115+ })
116+
117+ t.Run("plus expired and no prose returns expired error", func(t *testing.T) {
118+ mockDB := newMockFeatureDB()
119+ plusFF := db.NewFeatureFlag(userID, "plus", uint64(50*shared.MB), int64(5*shared.MB), int64(2*shared.KB))
120+ plusFF.ExpiresAt = &expiredExpires
121+ mockDB.setFeature(userID, plusFF)
122+
123+ _, err := findFeatureFlag(mockDB, cfg, userID)
124+ if err == nil {
125+ t.Fatalf("expected error, got nil")
126+ }
127+ if err.Error() != "ERROR: your pico+ has expired" {
128+ t.Errorf("unexpected error message: %v", err)
129+ }
130+ })
131+
132+ t.Run("prose expired and no plus returns expired error", func(t *testing.T) {
133+ mockDB := newMockFeatureDB()
134+ proseFF := db.NewFeatureFlag(userID, "prose", uint64(50*shared.MB), int64(5*shared.MB), int64(2*shared.KB))
135+ proseFF.ExpiresAt = &expiredExpires
136+ mockDB.setFeature(userID, proseFF)
137+
138+ _, err := findFeatureFlag(mockDB, cfg, userID)
139+ if err == nil {
140+ t.Fatalf("expected error, got nil")
141+ }
142+ if err.Error() != "ERROR: your prose access has expired" {
143+ t.Errorf("unexpected error message: %v", err)
144+ }
145+ })
146+
147+ t.Run("no flags returns required message", func(t *testing.T) {
148+ mockDB := newMockFeatureDB()
149+
150+ _, err := findFeatureFlag(mockDB, cfg, userID)
151+ if err == nil {
152+ t.Fatalf("expected error, got nil")
153+ }
154+ if err.Error() != "ERROR: uploading to prose requires the prose feature flag or pico+" {
155+ t.Errorf("unexpected error message: %v", err)
156+ }
157+ })
158+}
159+
160+func TestUploadHandlerValidate(t *testing.T) {
161+ cfg := NewConfigSite("prose-test")
162+ validExpires := time.Now().Add(24 * time.Hour)
163+ userID := "user-1"
164+
165+ createSession := func(user *db.User) *pssh.SSHServerConnSession {
166+ conn := &pssh.SSHServerConn{
167+ Conn: &ssh.ServerConn{
168+ Permissions: &ssh.Permissions{
169+ Extensions: map[string]string{},
170+ },
171+ },
172+ Logger: slog.Default(),
173+ }
174+ sesh := &pssh.SSHServerConnSession{
175+ SSHServerConn: conn,
176+ Ctx: context.Background(),
177+ }
178+ if user != nil {
179+ pssh.SetUser(sesh, user)
180+ }
181+ return sesh
182+ }
183+
184+ t.Run("nil user in session fails validation", func(t *testing.T) {
185+ mockDB := newMockFeatureDB()
186+ handler := NewUploadHandler(cfg, mockDB, map[string]filehandlers.ReadWriteHandler{})
187+
188+ sesh := createSession(nil)
189+ err := handler.Validate(sesh)
190+ if err == nil {
191+ t.Fatalf("expected error for nil user, got nil")
192+ }
193+ })
194+
195+ t.Run("user without feature flag fails validation", func(t *testing.T) {
196+ mockDB := newMockFeatureDB()
197+ handler := NewUploadHandler(cfg, mockDB, map[string]filehandlers.ReadWriteHandler{})
198+
199+ user := &db.User{ID: userID, Name: "tester"}
200+ sesh := createSession(user)
201+ err := handler.Validate(sesh)
202+ if err == nil {
203+ t.Fatalf("expected error for user without feature flag, got nil")
204+ }
205+ })
206+
207+ t.Run("user with prose feature flag passes validation and sets session flag", func(t *testing.T) {
208+ mockDB := newMockFeatureDB()
209+ proseFF := db.NewFeatureFlag(userID, "prose", uint64(50*shared.MB), int64(5*shared.MB), int64(2*shared.KB))
210+ proseFF.ExpiresAt = &validExpires
211+ mockDB.setFeature(userID, proseFF)
212+
213+ handler := NewUploadHandler(cfg, mockDB, map[string]filehandlers.ReadWriteHandler{})
214+
215+ user := &db.User{ID: userID, Name: "tester"}
216+ sesh := createSession(user)
217+ err := handler.Validate(sesh)
218+ if err != nil {
219+ t.Fatalf("unexpected error: %v", err)
220+ }
221+
222+ ff := getFeatureFlag(sesh)
223+ if ff == nil || ff.Name != "prose" {
224+ t.Errorf("expected session feature flag 'prose', got %v", ff)
225+ }
226+ })
227+}
Back to top