Commit da1f3af

Antonio Mika  ·  2026-10-10 22:14:55 -0400 EDT
parent cc428a5
refactor(apps): give each app its own config
73 files changed,  +681, -777
+1, -1
......@@ -9,7 +9,7 @@ import (
99 )
1010
1111 func main() {
12- cfg := feeds.NewConfigSite("feeds-fetch")
12+ cfg := feeds.NewConfig("feeds-fetch")
1313 dbh := feeds.NewDB(postgres.NewDB(cfg.DbURL, cfg.Logger))
1414 defer func() {
1515 _ = dbh.Close()
+1, -1
......@@ -26,7 +26,7 @@ func main() {
2626 defer func() {
2727 _ = pubsub.Close()
2828 }()
29- cfg := pgs.NewPgsConfig(logger, nil, nil, drain)
29+ cfg := pgs.NewConfig(logger, nil, nil, drain)
3030 proxy := newProxyServe(cfg.Logger)
3131 httpCache, cache := pgs.NewPgsHttpCache(cfg, proxy)
3232 cacher := &cachedHttp{
+1, -1
......@@ -29,7 +29,7 @@ func main() {
2929 defer func() {
3030 _ = pubsub.Close()
3131 }()
32- cfg := pgs.NewPgsConfig(logger, dbpool, st, pubsub)
32+ cfg := pgs.NewConfig(logger, dbpool, st, pubsub)
3333 killCh := make(chan error)
3434 pgs.StartSshServer(cfg, killCh)
3535 }
+1, -1
......@@ -27,7 +27,7 @@ func main() {
2727 defer func() {
2828 _ = pubsub.Close()
2929 }()
30- cfg := pgs.NewPgsConfig(logger, dbpool, st, pubsub)
30+ cfg := pgs.NewConfig(logger, dbpool, st, pubsub)
3131 killCh := make(chan error)
3232
3333 // first time user experience flow
+1, -1
......@@ -29,6 +29,6 @@ func main() {
2929 defer func() {
3030 _ = pubsub.Close()
3131 }()
32- cfg := pgs.NewPgsConfig(logger, dbpool, st, pubsub)
32+ cfg := pgs.NewConfig(logger, dbpool, st, pubsub)
3333 pgs.StartApiServer(cfg)
3434 }
+1, -4
......@@ -59,10 +59,7 @@ func updateDates(tx *sql.Tx, postID string, date *time.Time) error {
5959 func main() {
6060 logger := slog.Default()
6161
62- picoCfg := shared.NewConfigSite()
63- picoCfg.Logger = logger
64- picoCfg.DbURL = os.Getenv("DATABASE_URL")
65- picoDb := postgres.NewDB(picoCfg.DbURL, picoCfg.Logger)
62+ picoDb := postgres.NewDB(os.Getenv("DATABASE_URL"), logger)
6663
6764 logger.Info("fetching all posts")
6865 posts, err := findPosts(picoDb.Db)
+1, -5
......@@ -6,7 +6,6 @@ import (
66 "os"
77
88 "github.com/picosh/pico/pkg/db/postgres"
9- "github.com/picosh/pico/pkg/shared"
109 )
1110
1211 func bail(err error) {
......@@ -18,10 +17,7 @@ func bail(err error) {
1817 func main() {
1918 logger := slog.Default()
2019
21- picoCfg := shared.NewConfigSite()
22- picoCfg.Logger = logger
23- picoCfg.DbURL = os.Getenv("DATABASE_URL")
24- picoDb := postgres.NewDB(picoCfg.DbURL, picoCfg.Logger)
20+ picoDb := postgres.NewDB(os.Getenv("DATABASE_URL"), logger)
2521
2622 posts, err := picoDb.FindPosts()
2723 bail(err)
+3, -13
......@@ -10,7 +10,6 @@ import (
1010 "github.com/jmoiron/sqlx"
1111 "github.com/picosh/pico/pkg/db"
1212 "github.com/picosh/pico/pkg/db/postgres"
13- "github.com/picosh/pico/pkg/shared"
1413 )
1514
1615 func findPosts(dbpool *sqlx.DB) ([]*db.Post, error) {
......@@ -102,20 +101,11 @@ type ConflictData struct {
102101 func main() {
103102 logger := slog.Default()
104103
105- listsCfg := shared.NewConfigSite()
106- listsCfg.Logger = logger
107- listsCfg.DbURL = os.Getenv("LISTS_DB_URL")
108- listsDb := postgres.NewDB(listsCfg.DbURL, listsCfg.Logger)
104+ listsDb := postgres.NewDB(os.Getenv("LISTS_DB_URL"), logger)
109105
110- proseCfg := shared.NewConfigSite()
111- proseCfg.DbURL = os.Getenv("PROSE_DB_URL")
112- proseCfg.Logger = logger
113- proseDb := postgres.NewDB(proseCfg.DbURL, proseCfg.Logger)
106+ proseDb := postgres.NewDB(os.Getenv("PROSE_DB_URL"), logger)
114107
115- picoCfg := shared.NewConfigSite()
116- picoCfg.Logger = logger
117- picoCfg.DbURL = os.Getenv("PICO_DB_URL")
118- picoDb := postgres.NewDB(picoCfg.DbURL, picoCfg.Logger)
108+ picoDb := postgres.NewDB(os.Getenv("PICO_DB_URL"), logger)
119109
120110 ctx := context.Background()
121111 tx, err := picoDb.Db.BeginTx(ctx, nil)
+1, -4
......@@ -10,10 +10,7 @@ import (
1010
1111 func main() {
1212 logger := slog.Default()
13- picoCfg := shared.NewConfigSite()
14- picoCfg.Logger = logger
15- picoCfg.DbURL = os.Getenv("DATABASE_URL")
16- picoDb := postgres.NewDB(picoCfg.DbURL, picoCfg.Logger)
13+ picoDb := postgres.NewDB(os.Getenv("DATABASE_URL"), logger)
1714
1815 logger.Info("fetching all posts")
1916 posts, err := picoDb.FindPosts()
+1, -4
......@@ -51,10 +51,7 @@ func findPosts(dbpool *sqlx.DB) ([]*db.Post, error) {
5151 func main() {
5252 logger := slog.Default()
5353
54- picoCfg := shared.NewConfigSite()
55- picoCfg.Logger = logger
56- picoCfg.DbURL = os.Getenv("DATABASE_URL")
57- picoDb := postgres.NewDB(picoCfg.DbURL, picoCfg.Logger)
54+ picoDb := postgres.NewDB(os.Getenv("DATABASE_URL"), logger)
5855
5956 logger.Info("fetching all posts")
6057 posts, err := findPosts(picoDb.Db)
+4, -17
......@@ -39,7 +39,7 @@ type oauth2Server struct {
3939 ResponseTypesSupported []string `json:"response_types_supported"`
4040 }
4141
42-func generateURL(cfg *shared.ConfigSite, path string, space string) string {
42+func generateURL(cfg *Config, path string, space string) string {
4343 query := ""
4444
4545 if space != "" {
......@@ -50,7 +50,7 @@ func generateURL(cfg *shared.ConfigSite, path string, space string) string {
5050 }
5151
5252 type ApiConfig struct {
53- Cfg *shared.ConfigSite
53+ Cfg *Config
5454 Dbpool db.DB
5555 }
5656
......@@ -919,18 +919,7 @@ func authMux(apiConfig *ApiConfig) *http.ServeMux {
919919 }
920920
921921 func StartApiServer() {
922- debug := shared.GetEnv("AUTH_DEBUG", "0")
923- withPipe := strings.ToLower(shared.GetEnv("PICO_PIPE_ENABLED", "true")) == "true"
924-
925- cfg := &shared.ConfigSite{
926- DbURL: shared.GetEnv("DATABASE_URL", ""),
927- Debug: debug == "1",
928- Issuer: shared.GetEnv("AUTH_ISSUER", "pico.sh"),
929- Domain: shared.GetEnv("AUTH_DOMAIN", "http://0.0.0.0:3000"),
930- Port: shared.GetEnv("AUTH_WEB_PORT", "3000"),
931- Secret: shared.GetEnv("PICO_SECRET", ""),
932- SecretWebhook: shared.GetEnv("PICO_SECRET_WEBHOOK", ""),
933- }
922+ cfg := NewConfig()
934923
935924 if cfg.SecretWebhook == "" {
936925 panic("must provide PICO_SECRET_WEBHOOK environment variable")
......@@ -940,9 +929,7 @@ func StartApiServer() {
940929 panic("must provide PICO_SECRET environment variable")
941930 }
942931
943- logger := shared.CreateLogger("auth-web", withPipe)
944-
945- cfg.Logger = logger
932+ logger := cfg.Logger
946933
947934 db := postgres.NewDB(cfg.DbURL, logger)
948935 defer func() {
+1, -1
......@@ -279,7 +279,7 @@ func mkpath(path string) string {
279279
280280 func setupTest() *ApiConfig {
281281 logger := shared.CreateLogger("auth-test", false)
282- cfg := &shared.ConfigSite{
282+ cfg := &Config{
283283 Issuer: "auth.pico.test",
284284 Domain: "http://0.0.0.0:3000",
285285 Port: "3000",
+35, -0
......@@ -0,0 +1,35 @@
1+package auth
2+
3+import (
4+ "log/slog"
5+ "strings"
6+
7+ "github.com/picosh/pico/pkg/shared"
8+)
9+
10+type Config struct {
11+ Debug bool
12+ DbURL string
13+ Issuer string
14+ Domain string
15+ Port string
16+ Secret string
17+ SecretWebhook string
18+ Logger *slog.Logger
19+}
20+
21+func NewConfig() *Config {
22+ debug := shared.GetEnv("AUTH_DEBUG", "0")
23+ withPipe := strings.ToLower(shared.GetEnv("PICO_PIPE_ENABLED", "true")) == "true"
24+
25+ return &Config{
26+ DbURL: shared.GetEnv("DATABASE_URL", ""),
27+ Debug: debug == "1",
28+ Issuer: shared.GetEnv("AUTH_ISSUER", "pico.sh"),
29+ Domain: shared.GetEnv("AUTH_DOMAIN", "http://0.0.0.0:3000"),
30+ Port: shared.GetEnv("AUTH_WEB_PORT", "3000"),
31+ Secret: shared.GetEnv("PICO_SECRET", ""),
32+ SecretWebhook: shared.GetEnv("PICO_SECRET_WEBHOOK", ""),
33+ Logger: shared.CreateLogger("auth-web", withPipe),
34+ }
35+}
+10, -9
......@@ -98,23 +98,24 @@ func (web *webRouter) unsubHandler(w http.ResponseWriter, r *http.Request) {
9898 }
9999
100100 type webRouter struct {
101- cfg *shared.ConfigSite
101+ cfg *Config
102102 db DB
103103 }
104104
105105 func (web *webRouter) mux() *http.ServeMux {
106106 cfg := web.cfg
107+ files := cfg.Static()
107108 mux := http.NewServeMux()
108- mux.HandleFunc("GET /{$}", router.CreatePageHandler(cfg, "html/marketing.page.tmpl"))
109+ mux.HandleFunc("GET /{$}", files.Page("html/marketing.page.tmpl"))
109110 mux.HandleFunc("GET /keep-alive/{id...}", web.keepAliveHandler)
110111 mux.HandleFunc("GET /unsub/{id...}", web.unsubHandler)
111112 mux.Handle("GET /_metrics", promhttp.Handler())
112- mux.HandleFunc("GET /main.css", router.ServeFile(cfg, "main.css", "text/css"))
113- mux.HandleFunc("GET /card.png", router.ServeFile(cfg, "card.png", "image/png"))
114- mux.HandleFunc("GET /favicon-16x16.png", router.ServeFile(cfg, "favicon-16x16.png", "image/png"))
115- mux.HandleFunc("GET /apple-touch-icon.png", router.ServeFile(cfg, "apple-touch-icon.png", "image/png"))
116- mux.HandleFunc("GET /favicon.ico", router.ServeFile(cfg, "favicon.ico", "image/x-icon"))
117- mux.HandleFunc("GET /robots.txt", router.ServeFile(cfg, "robots.txt", "text/plain"))
113+ mux.HandleFunc("GET /main.css", files.File("main.css", "text/css"))
114+ mux.HandleFunc("GET /card.png", files.File("card.png", "image/png"))
115+ mux.HandleFunc("GET /favicon-16x16.png", files.File("favicon-16x16.png", "image/png"))
116+ mux.HandleFunc("GET /apple-touch-icon.png", files.File("apple-touch-icon.png", "image/png"))
117+ mux.HandleFunc("GET /favicon.ico", files.File("favicon.ico", "image/x-icon"))
118+ mux.HandleFunc("GET /robots.txt", files.File("robots.txt", "text/plain"))
118119 if cfg.Debug {
119120 router.CreatePProfRoutesMux(mux)
120121 }
......@@ -122,7 +123,7 @@ func (web *webRouter) mux() *http.ServeMux {
122123 }
123124
124125 func StartApiServer() {
125- cfg := NewConfigSite("feeds-web")
126+ cfg := NewConfig("feeds-web")
126127 db := NewDB(postgres.NewDB(cfg.DbURL, cfg.Logger))
127128 defer func() {
128129 _ = db.Close()
+1, -1
......@@ -12,7 +12,7 @@ import (
1212
1313 func TestRoutes(t *testing.T) {
1414 t.Chdir("../../..")
15- cfg := NewConfigSite("feeds-test")
15+ cfg := NewConfig("feeds-test")
1616 cfg.Logger = slog.New(slog.DiscardHandler)
1717 web := &webRouter{cfg: cfg, db: stub.NewPostsDB(cfg.Logger, &db.User{ID: "user-1"})}
1818 mux := web.mux()
+1, -1
......@@ -11,7 +11,7 @@ import (
1111 "github.com/picosh/pico/pkg/shared"
1212 )
1313
14-func Middleware(dbpool DB, cfg *shared.ConfigSite) pssh.SSHServerMiddleware {
14+func Middleware(dbpool DB, cfg *Config) pssh.SSHServerMiddleware {
1515 return func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
1616 return func(sesh *pssh.SSHServerConnSession) error {
1717 args := sesh.Command()
+36, -3
......@@ -1,12 +1,31 @@
11 package feeds
22
33 import (
4+ "html/template"
5+ "log/slog"
6+ "path/filepath"
47 "strings"
58
69 "github.com/picosh/pico/pkg/shared"
10+ "github.com/picosh/pico/pkg/shared/router"
711 )
812
9-func NewConfigSite(service string) *shared.ConfigSite {
13+// staticDir holds feeds' templates and public files.
14+const staticDir = "pkg/apps/feeds"
15+
16+type Config struct {
17+ Debug bool
18+ SendgridKey string
19+ Domain string
20+ Port string
21+ Protocol string
22+ DbURL string
23+ Space string
24+ AllowedExt []string
25+ Logger *slog.Logger
26+}
27+
28+func NewConfig(service string) *Config {
1029 debug := shared.GetEnv("FEEDS_DEBUG", "0")
1130 domain := shared.GetEnv("FEEDS_DOMAIN", "feeds.pico.sh")
1231 port := shared.GetEnv("FEEDS_WEB_PORT", "3000")
......@@ -15,7 +34,7 @@ func NewConfigSite(service string) *shared.ConfigSite {
1534 sendgridKey := shared.GetEnv("SENDGRID_API_KEY", "")
1635 withPipe := strings.ToLower(shared.GetEnv("PICO_PIPE_ENABLED", "true")) == "true"
1736
18- return &shared.ConfigSite{
37+ return &Config{
1938 Debug: debug == "1",
2039 SendgridKey: sendgridKey,
2140 Domain: domain,
......@@ -24,7 +43,21 @@ func NewConfigSite(service string) *shared.ConfigSite {
2443 DbURL: dbURL,
2544 Space: "feeds",
2645 AllowedExt: []string{".txt", ".lxt"},
27- HiddenPosts: []string{},
2846 Logger: shared.CreateLogger(service, withPipe),
2947 }
3048 }
49+
50+func (c *Config) StaticPath(fname string) string {
51+ return filepath.Join(staticDir, fname)
52+}
53+
54+func (c *Config) Static() router.Static {
55+ return router.Static{
56+ Dir: staticDir,
57+ Logger: c.Logger,
58+ Site: shared.SitePageData{
59+ Domain: template.URL(c.Domain),
60+ HomeURL: template.URL("//" + c.Domain),
61+ },
62+ }
63+}
+2, -2
......@@ -128,14 +128,14 @@ func isValidItem(logger *slog.Logger, item *gofeed.Item, feedItems []*db.FeedIte
128128 }
129129
130130 type Fetcher struct {
131- cfg *shared.ConfigSite
131+ cfg *Config
132132 db DB
133133 auth sasl.Client
134134 gron *gronx.Gronx
135135 host string
136136 }
137137
138-func NewFetcher(dbpool DB, cfg *shared.ConfigSite) *Fetcher {
138+func NewFetcher(dbpool DB, cfg *Config) *Fetcher {
139139 host := os.Getenv("PICO_SMTP_HOST")
140140 smtPass := os.Getenv("PICO_SMTP_PASS")
141141 emailLogin := os.Getenv("PICO_SMTP_USER")
+1, -1
......@@ -23,7 +23,7 @@ func StartSshServer() {
2323 host := shared.GetEnv("FEEDS_HOST", "0.0.0.0")
2424 port := shared.GetEnv("FEEDS_SSH_PORT", "2222")
2525 promPort := shared.GetEnv("FEEDS_PROM_PORT", "9222")
26- cfg := NewConfigSite(appName)
26+ cfg := NewConfig(appName)
2727 logger := cfg.Logger
2828
2929 ctx, cancel := context.WithCancel(context.Background())
+3, -3
......@@ -19,7 +19,7 @@ import (
1919
2020 // UploadHandler stores every file as a feed.
2121 type UploadHandler struct {
22- Cfg *shared.ConfigSite
22+ Cfg *Config
2323 DB DB
2424 }
2525
......@@ -29,7 +29,7 @@ var (
2929 _ sendutils.FileRenamer = (*UploadHandler)(nil)
3030 )
3131
32-func NewUploadHandler(cfg *shared.ConfigSite, dbpool DB) *UploadHandler {
32+func NewUploadHandler(cfg *Config, dbpool DB) *UploadHandler {
3333 return &UploadHandler{Cfg: cfg, DB: dbpool}
3434 }
3535
......@@ -100,7 +100,7 @@ func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.Fil
100100 return "", err
101101 }
102102 logger.Info("saved feed", "new", cur == nil, "changed", changed)
103- return h.Cfg.FullPostURL(shared.NewCreateURL(h.Cfg), user.Name, post.Slug), nil
103+ return shared.NewCreateURL(h.Cfg.Protocol, h.Cfg.Domain).FullPostURL(user.Name, post.Slug), nil
104104 }
105105
106106 // Rename moves a feed to a new filename in place, keeping its digest state.
+1, -1
......@@ -26,7 +26,7 @@ func TestWriteKeepsDigestState(t *testing.T) {
2626 user := &db.User{ID: "user-1", Name: "tester"}
2727 pssh.SetUser(session, user)
2828 posts := stub.NewPostsDB(logger, user)
29- cfg := NewConfigSite("feeds-test")
29+ cfg := NewConfig("feeds-test")
3030 h := NewUploadHandler(cfg, posts)
3131 write := func(text string, mtime int64) {
3232 t.Helper()
+27, -26
......@@ -72,7 +72,7 @@ type HeaderTxt struct {
7272 }
7373
7474 func (web *webRouter) blogHandler(w http.ResponseWriter, r *http.Request) {
75- username := router.GetUsernameFromRequest(r, web.cfg)
75+ username := router.GetUsernameFromRequest(r)
7676 dbpool := web.db
7777 blogger := web.cfg.Logger
7878 logger := blogger.With("user", username)
......@@ -95,7 +95,7 @@ func (web *webRouter) blogHandler(w http.ResponseWriter, r *http.Request) {
9595
9696 posts := pager.Data
9797
98- ts, err := router.RenderTemplate(cfg, []string{
98+ ts, err := router.RenderTemplate(staticDir, []string{
9999 cfg.StaticPath("html/blog.page.tmpl"),
100100 })
101101
......@@ -110,12 +110,12 @@ func (web *webRouter) blogHandler(w http.ResponseWriter, r *http.Request) {
110110 Bio: "",
111111 }
112112
113- curl := shared.CreateURLFromRequest(cfg, r)
113+ curl := shared.CreateURLFromRequest(cfg.Protocol, cfg.Domain, r)
114114 postCollection := make([]PostItemData, 0, len(posts))
115115 for _, post := range posts {
116116 p := PostItemData{
117- URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
118- BlogURL: template.URL(cfg.FullBlogURL(curl, post.Username)),
117+ URL: template.URL(curl.FullPostURL(post.Username, post.Slug)),
118+ BlogURL: template.URL(curl.FullBlogURL(post.Username)),
119119 Title: post.Filename,
120120 PublishAt: post.PublishAt.Format(time.DateOnly),
121121 PublishAtISO: post.PublishAt.Format(time.RFC3339),
......@@ -126,10 +126,10 @@ func (web *webRouter) blogHandler(w http.ResponseWriter, r *http.Request) {
126126 }
127127
128128 data := BlogPageData{
129- Site: *cfg.GetSiteData(),
129+ Site: cfg.SiteData(),
130130 PageTitle: headerTxt.Title,
131- URL: template.URL(cfg.FullBlogURL(curl, username)),
132- RSSURL: template.URL(cfg.RssBlogURL(curl, username, "")),
131+ URL: template.URL(curl.FullBlogURL(username)),
132+ RSSURL: template.URL(curl.RssBlogURL(username, "")),
133133 Header: headerTxt,
134134 Username: username,
135135 Posts: postCollection,
......@@ -155,7 +155,7 @@ func GetBlogName(username string) string {
155155 }
156156
157157 func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
158- username := router.GetUsernameFromRequest(r, web.cfg)
158+ username := router.GetUsernameFromRequest(r)
159159 cfg := web.cfg
160160 slug, _ := url.PathUnescape(r.PathValue("slug"))
161161
......@@ -201,7 +201,7 @@ func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
201201 }
202202
203203 data = PostPageData{
204- Site: *cfg.GetSiteData(),
204+ Site: cfg.SiteData(),
205205 PageTitle: post.Filename,
206206 URL: template.URL(cfg.PostURL(post.Username, post.Slug)),
207207 RawURL: template.URL(cfg.RawPostURL(post.Username, post.Slug)),
......@@ -220,7 +220,7 @@ func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
220220 } else {
221221 logger.Info("paste not found")
222222 data = PostPageData{
223- Site: *cfg.GetSiteData(),
223+ Site: cfg.SiteData(),
224224 PageTitle: "Paste not found",
225225 Description: "Paste not found",
226226 Title: "Paste not found",
......@@ -235,7 +235,7 @@ func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
235235 }
236236 }
237237
238- ts, err := router.RenderTemplate(cfg, []string{
238+ ts, err := router.RenderTemplate(staticDir, []string{
239239 cfg.StaticPath("html/post.page.tmpl"),
240240 })
241241
......@@ -252,7 +252,7 @@ func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
252252 }
253253
254254 func (web *webRouter) postHandlerRaw(w http.ResponseWriter, r *http.Request) {
255- username := router.GetUsernameFromRequest(r, web.cfg)
255+ username := router.GetUsernameFromRequest(r)
256256 cfg := web.cfg
257257 slug, _ := url.PathUnescape(r.PathValue("slug"))
258258
......@@ -285,20 +285,21 @@ func (web *webRouter) postHandlerRaw(w http.ResponseWriter, r *http.Request) {
285285 }
286286
287287 type webRouter struct {
288- cfg *shared.ConfigSite
288+ cfg *Config
289289 db DB
290290 }
291291
292292 func (web *webRouter) static(mux *http.ServeMux) {
293293 cfg := web.cfg
294- mux.HandleFunc("GET /main.css", router.ServeFile(cfg, "main.css", "text/css"))
295- mux.HandleFunc("GET /smol.css", router.ServeFile(cfg, "smol.css", "text/css"))
296- mux.HandleFunc("GET /syntax.css", router.ServeFile(cfg, "syntax.css", "text/css"))
297- mux.HandleFunc("GET /card.png", router.ServeFile(cfg, "card.png", "image/png"))
298- mux.HandleFunc("GET /favicon-16x16.png", router.ServeFile(cfg, "favicon-16x16.png", "image/png"))
299- mux.HandleFunc("GET /apple-touch-icon.png", router.ServeFile(cfg, "apple-touch-icon.png", "image/png"))
300- mux.HandleFunc("GET /favicon.ico", router.ServeFile(cfg, "favicon.ico", "image/x-icon"))
301- mux.HandleFunc("GET /robots.txt", router.ServeFile(cfg, "robots.txt", "text/plain"))
294+ files := cfg.Static()
295+ mux.HandleFunc("GET /main.css", files.File("main.css", "text/css"))
296+ mux.HandleFunc("GET /smol.css", files.File("smol.css", "text/css"))
297+ mux.HandleFunc("GET /syntax.css", files.File("syntax.css", "text/css"))
298+ mux.HandleFunc("GET /card.png", files.File("card.png", "image/png"))
299+ mux.HandleFunc("GET /favicon-16x16.png", files.File("favicon-16x16.png", "image/png"))
300+ mux.HandleFunc("GET /apple-touch-icon.png", files.File("apple-touch-icon.png", "image/png"))
301+ mux.HandleFunc("GET /favicon.ico", files.File("favicon.ico", "image/x-icon"))
302+ mux.HandleFunc("GET /robots.txt", files.File("robots.txt", "text/plain"))
302303 if cfg.Debug {
303304 router.CreatePProfRoutesMux(mux)
304305 }
......@@ -307,8 +308,8 @@ func (web *webRouter) static(mux *http.ServeMux) {
307308 // mainMux serves the marketing site and users' pastes below /{user}.
308309 func (web *webRouter) mainMux() *http.ServeMux {
309310 mux := http.NewServeMux()
310- mux.HandleFunc("GET /{$}", router.CreatePageHandler(web.cfg, "html/marketing.page.tmpl"))
311- mux.HandleFunc("GET /check", router.CheckHandler(web.cfg, web.db))
311+ mux.HandleFunc("GET /{$}", web.cfg.Static().Page("html/marketing.page.tmpl"))
312+ mux.HandleFunc("GET /check", router.CheckHandler(web.cfg.Domain, web.cfg.Space, web.db))
312313 mux.Handle("GET /_metrics", promhttp.Handler())
313314 web.static(mux)
314315 mux.HandleFunc("GET /{user}", web.blogHandler)
......@@ -352,7 +353,7 @@ func (web *webRouter) rawRoute(w http.ResponseWriter, r *http.Request) {
352353 }
353354
354355 func StartApiServer() {
355- cfg := NewConfigSite("pastes-web")
356+ cfg := NewConfig("pastes-web")
356357 db := NewDB(postgres.NewDB(cfg.DbURL, cfg.Logger))
357358 defer func() {
358359 _ = db.Close()
......@@ -362,7 +363,7 @@ func StartApiServer() {
362363 go CronDeleteExpiredPosts(cfg, db)
363364
364365 web := &webRouter{cfg: cfg, db: db}
365- handler := router.NewHostRouter(cfg, web.mainMux(), web.sitesMux())
366+ handler := router.NewHostRouter(cfg.Domain, cfg.Space, web.mainMux(), web.sitesMux())
366367
367368 portStr := fmt.Sprintf(":%s", cfg.Port)
368369 logger.Info(
+4, -4
......@@ -35,7 +35,7 @@ func (m *pasteDB) FindPostWithSlug(slug string, _ string, _ string) (*db.Post, e
3535 func TestLargePasteEscapesHTML(t *testing.T) {
3636 t.Chdir("../../..")
3737 logger := slog.New(slog.DiscardHandler)
38- cfg := &shared.ConfigSite{
38+ cfg := &Config{
3939 Domain: "pastes.test", Protocol: "https", Space: "pastes", Logger: logger,
4040 }
4141 now := time.Now()
......@@ -48,7 +48,7 @@ func TestLargePasteEscapesHTML(t *testing.T) {
4848 },
4949 }
5050 web := &webRouter{cfg: cfg, db: dbpool}
51- handler := router.NewHostRouter(cfg, web.mainMux(), web.sitesMux())
51+ handler := router.NewHostRouter(cfg.Domain, cfg.Space, web.mainMux(), web.sitesMux())
5252
5353 page := httptest.NewRecorder()
5454 handler.ServeHTTP(page, httptest.NewRequest(http.MethodGet, "https://tester.pastes.test/test.html", nil))
......@@ -73,7 +73,7 @@ func TestLargePasteEscapesHTML(t *testing.T) {
7373 func TestRoutes(t *testing.T) {
7474 t.Chdir("../../..")
7575 logger := slog.New(slog.DiscardHandler)
76- cfg := &shared.ConfigSite{
76+ cfg := &Config{
7777 Domain: "pastes.test", Protocol: "https", Space: "pastes", Logger: logger,
7878 }
7979 now := time.Now()
......@@ -85,7 +85,7 @@ func TestRoutes(t *testing.T) {
8585 },
8686 }
8787 web := &webRouter{cfg: cfg, db: dbpool}
88- handler := router.NewHostRouter(cfg, web.mainMux(), web.sitesMux())
88+ handler := router.NewHostRouter(cfg.Domain, cfg.Space, web.mainMux(), web.sitesMux())
8989
9090 for _, tt := range []struct {
9191 url string
+49, -2
......@@ -1,12 +1,32 @@
11 package pastes
22
33 import (
4+ "fmt"
5+ "html/template"
6+ "log/slog"
7+ "net/url"
8+ "path/filepath"
49 "strings"
510
611 "github.com/picosh/pico/pkg/shared"
12+ "github.com/picosh/pico/pkg/shared/router"
713 )
814
9-func NewConfigSite(service string) *shared.ConfigSite {
15+// staticDir holds pastes' templates and public files.
16+const staticDir = "pkg/apps/pastes"
17+
18+type Config struct {
19+ Debug bool
20+ Domain string
21+ Port string
22+ Protocol string
23+ DbURL string
24+ Space string
25+ MaxAssetSize int64
26+ Logger *slog.Logger
27+}
28+
29+func NewConfig(service string) *Config {
1030 debug := shared.GetEnv("PASTES_DEBUG", "0")
1131 domain := shared.GetEnv("PASTES_DOMAIN", "pastes.sh")
1232 port := shared.GetEnv("PASTES_WEB_PORT", "3000")
......@@ -14,7 +34,7 @@ func NewConfigSite(service string) *shared.ConfigSite {
1434 protocol := shared.GetEnv("PASTES_PROTOCOL", "https")
1535 withPipe := strings.ToLower(shared.GetEnv("PICO_PIPE_ENABLED", "true")) == "true"
1636
17- return &shared.ConfigSite{
37+ return &Config{
1838 Debug: debug == "1",
1939 Domain: domain,
2040 Port: port,
......@@ -25,3 +45,30 @@ func NewConfigSite(service string) *shared.ConfigSite {
2545 MaxAssetSize: int64(3 * shared.MB),
2646 }
2747 }
48+
49+func (c *Config) StaticPath(fname string) string {
50+ return filepath.Join(staticDir, fname)
51+}
52+
53+func (c *Config) SiteData() shared.SitePageData {
54+ return shared.SitePageData{
55+ Domain: template.URL(c.Domain),
56+ HomeURL: template.URL("//" + c.Domain),
57+ }
58+}
59+
60+func (c *Config) Static() router.Static {
61+ return router.Static{Dir: staticDir, Logger: c.Logger, Site: c.SiteData()}
62+}
63+
64+func (c *Config) BlogURL(username string) string {
65+ return fmt.Sprintf("%s://%s.%s", c.Protocol, username, c.Domain)
66+}
67+
68+func (c *Config) PostURL(username, slug string) string {
69+ return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, url.PathEscape(slug))
70+}
71+
72+func (c *Config) RawPostURL(username, slug string) string {
73+ return fmt.Sprintf("%s://%s.%s/raw/%s", c.Protocol, username, c.Domain, url.PathEscape(slug))
74+}
+2, -4
......@@ -2,11 +2,9 @@ package pastes
22
33 import (
44 "time"
5-
6- "github.com/picosh/pico/pkg/shared"
75 )
86
9-func deleteExpiredPosts(cfg *shared.ConfigSite, dbpool DB) error {
7+func deleteExpiredPosts(cfg *Config, dbpool DB) error {
108 cfg.Logger.Info("checking for expired posts")
119 posts, err := dbpool.FindExpiredPosts(cfg.Space)
1210 if err != nil {
......@@ -27,7 +25,7 @@ func deleteExpiredPosts(cfg *shared.ConfigSite, dbpool DB) error {
2725 return nil
2826 }
2927
30-func CronDeleteExpiredPosts(cfg *shared.ConfigSite, dbpool DB) {
28+func CronDeleteExpiredPosts(cfg *Config, dbpool DB) {
3129 for {
3230 err := deleteExpiredPosts(cfg, dbpool)
3331 if err != nil {
+1, -1
......@@ -24,7 +24,7 @@ func StartSshServer() {
2424 host := shared.GetEnv("PASTES_HOST", "0.0.0.0")
2525 port := shared.GetEnv("PASTES_SSH_PORT", "2222")
2626 promPort := shared.GetEnv("PASTES_PROM_PORT", "9222")
27- cfg := NewConfigSite(appName)
27+ cfg := NewConfig(appName)
2828 logger := cfg.Logger
2929
3030 ctx, cancel := context.WithCancel(context.Background())
+4, -24
......@@ -19,29 +19,9 @@ import (
1919
2020 var DEFAULT_EXPIRES_AT = 90
2121
22-func checkAccess(dbpool DB, userID string) error {
23- ffPlus, plusErr := dbpool.FindFeature(userID, "plus")
24- if plusErr == nil && ffPlus.IsValid() {
25- return nil
26- }
27-
28- ffPastes, pastesErr := dbpool.FindFeature(userID, "pastes")
29- if pastesErr == nil {
30- if ffPastes.IsValid() {
31- return nil
32- }
33- return fmt.Errorf("ERROR: your pastes access has expired: https://blog.pico.sh/ann-038-pico-invite-system")
34- }
35-
36- if plusErr == nil {
37- return fmt.Errorf("ERROR: your pico+ has expired: https://blog.pico.sh/ann-038-pico-invite-system")
38- }
39- return fmt.Errorf("ERROR: uploading to pastes requires an invitation or pico+: https://blog.pico.sh/ann-038-pico-invite-system")
40-}
41-
4222 // UploadHandler stores every file as a paste.
4323 type UploadHandler struct {
44- Cfg *shared.ConfigSite
24+ Cfg *Config
4525 DB DB
4626 }
4727
......@@ -51,7 +31,7 @@ var (
5131 _ sendutils.FileRenamer = (*UploadHandler)(nil)
5232 )
5333
54-func NewUploadHandler(cfg *shared.ConfigSite, dbpool DB) *UploadHandler {
34+func NewUploadHandler(cfg *Config, dbpool DB) *UploadHandler {
5535 return &UploadHandler{Cfg: cfg, DB: dbpool}
5636 }
5737
......@@ -71,7 +51,7 @@ func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
7151 if err != nil {
7252 return err
7353 }
74- if err := checkAccess(h.DB, user.ID); err != nil {
54+ if _, err := db.FindAccess(h.DB, user.ID, "pastes"); err != nil {
7555 return err
7656 }
7757 h.GetLogger(s).Info("attempting to upload files", "user", user.Name, "space", h.Cfg.Space)
......@@ -132,7 +112,7 @@ func (h *UploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.Fil
132112 return "", err
133113 }
134114 logger.Info("saved paste", "new", cur == nil, "changed", changed)
135- return h.Cfg.FullPostURL(shared.NewCreateURL(h.Cfg), user.Name, post.Slug), nil
115+ return shared.NewCreateURL(h.Cfg.Protocol, h.Cfg.Domain).FullPostURL(user.Name, post.Slug), nil
136116 }
137117
138118 // Rename moves a paste to a new filename in place, keeping its settings.
+2, -3
......@@ -15,7 +15,6 @@ import (
1515 "github.com/picosh/pico/pkg/db/stub"
1616 "github.com/picosh/pico/pkg/pssh"
1717 sendutils "github.com/picosh/pico/pkg/send/utils"
18- "github.com/picosh/pico/pkg/shared"
1918 "golang.org/x/crypto/ssh"
2019 )
2120
......@@ -43,7 +42,7 @@ func (m *mockFeatureDB) setFeature(userID, name string, expiresAt time.Time) {
4342 }
4443
4544 func TestUploadHandlerValidate(t *testing.T) {
46- cfg := NewConfigSite("pastes-test")
45+ cfg := NewConfig("pastes-test")
4746 userID := "user-1"
4847 future := time.Now().Add(24 * time.Hour)
4948 past := time.Now().Add(-24 * time.Hour)
......@@ -198,7 +197,7 @@ func TestWriteReadError(t *testing.T) {
198197 user := &db.User{ID: "user-1", Name: "tester"}
199198 pssh.SetUser(session, user)
200199 posts := stub.NewPostsDB(logger, user)
201- handler := NewUploadHandler(&shared.ConfigSite{Space: "pastes", MaxAssetSize: 1024}, posts)
200+ handler := NewUploadHandler(&Config{Space: "pastes", MaxAssetSize: 1024}, posts)
202201 reader := io.MultiReader(strings.NewReader(tt.text), iotest.ErrReader(tt.err))
203202
204203 message, err := handler.Write(session, &sendutils.FileEntry{Filepath: "/test.txt", Reader: reader})
+1, -1
......@@ -36,7 +36,7 @@ func TestPrivateProjectDeniesWebAccess(t *testing.T) {
3636 defer func() {
3737 _ = pubsub.Close()
3838 }()
39- cfg := NewPgsConfig(logger, dbpool, st, pubsub)
39+ cfg := NewConfig(logger, dbpool, st, pubsub)
4040 cfg.Domain = "pgs.test"
4141 router := NewWebRouter(cfg)
4242 router.ServeHTTP(responseRecorder, request)
+3, -3
......@@ -6,7 +6,7 @@ import (
66 "github.com/picosh/pico/pkg/shared/router"
77 )
88
9-func handleAutoForm(w http.ResponseWriter, r *http.Request, cfg *PgsConfig) {
9+func handleAutoForm(w http.ResponseWriter, r *http.Request, cfg *Config) {
1010 formName := r.PathValue("fname")
1111 if r.Method != http.MethodPost {
1212 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
......@@ -65,7 +65,7 @@ type FormData struct {
6565 Error string
6666 }
6767
68-func serveAutoFormSubmitted(w http.ResponseWriter, r *http.Request, cfg *PgsConfig) {
68+func serveAutoFormSubmitted(w http.ResponseWriter, r *http.Request, cfg *Config) {
6969 errorMsg := r.URL.Query().Get("error")
7070 data := loginFormData{
7171 Error: errorMsg,
......@@ -73,7 +73,7 @@ func serveAutoFormSubmitted(w http.ResponseWriter, r *http.Request, cfg *PgsConf
7373
7474 w.WriteHeader(http.StatusUnprocessableEntity)
7575
76- ts, err := renderTemplate(cfg, []string{cfg.StaticPath("html/auto_form.page.tmpl")})
76+ ts, err := router.RenderTemplate(staticDir, []string{cfg.StaticPath("html/auto_form.page.tmpl")})
7777 if err != nil {
7878 cfg.Logger.Error("could not render auto form template", "err", err.Error())
7979 http.Error(w, "Server error", http.StatusInternalServerError)
+1, -1
......@@ -54,7 +54,7 @@ type Cmd struct {
5454 Write bool
5555 Width int
5656 Height int
57- Cfg *PgsConfig
57+ Cfg *Config
5858 }
5959
6060 func (c *Cmd) output(out string) {
+2, -2
......@@ -34,7 +34,7 @@ func TestCliPurges(t *testing.T) {
3434 if err != nil {
3535 t.Fatal(err)
3636 }
37- cfg := NewPgsConfig(logger, dbpool, st, discardPubsub{})
37+ cfg := NewConfig(logger, dbpool, st, discardPubsub{})
3838 cmd := &Cmd{User: user, Session: &fakeCmdSession{}, Log: logger, Store: st, Dbpool: dbpool, Write: true, Cfg: cfg}
3939
4040 tests := []struct {
......@@ -88,7 +88,7 @@ func TestRmProjectAssetsRemovesDirs(t *testing.T) {
8888 t.Fatal(err)
8989 }
9090
91- cfg := NewPgsConfig(logger, dbpool, st, discardPubsub{})
91+ cfg := NewConfig(logger, dbpool, st, discardPubsub{})
9292 cmd := &Cmd{User: user, Session: &fakeCmdSession{}, Log: logger, Store: st, Dbpool: dbpool, Write: true, Cfg: cfg}
9393 if err := cmd.RmProjectAssets("test"); err != nil {
9494 t.Fatal(err)
+19, -6
......@@ -2,6 +2,7 @@ package pgs
22
33 import (
44 "fmt"
5+ "html/template"
56 "log/slog"
67 "path/filepath"
78 "strconv"
......@@ -9,10 +10,11 @@ import (
910
1011 pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
1112 "github.com/picosh/pico/pkg/shared"
13+ "github.com/picosh/pico/pkg/shared/router"
1214 "github.com/picosh/pico/pkg/storage"
1315 )
1416
15-type PgsConfig struct {
17+type Config struct {
1618 CacheTTL time.Duration
1719 CacheMaxItems int
1820 CacheMaxBodySize int64
......@@ -38,7 +40,7 @@ type PgsConfig struct {
3840 Pubsub PicoPubsub
3941 }
4042
41-func (c *PgsConfig) AssetURL(username, projectName, fpath string) string {
43+func (c *Config) AssetURL(username, projectName, fpath string) string {
4244 if username == projectName {
4345 return fmt.Sprintf(
4446 "%s://%s.%s/%s",
......@@ -59,8 +61,19 @@ func (c *PgsConfig) AssetURL(username, projectName, fpath string) string {
5961 )
6062 }
6163
62-func (c *PgsConfig) StaticPath(fname string) string {
63- return filepath.Join("pkg", "apps", "pgs", fname)
64+// staticDir holds pgs's templates and public files.
65+const staticDir = "pkg/apps/pgs"
66+
67+func (c *Config) StaticPath(fname string) string {
68+ return filepath.Join(staticDir, fname)
69+}
70+
71+func (c *Config) Static() router.Static {
72+ return router.Static{
73+ Dir: staticDir,
74+ Logger: c.Logger,
75+ Site: shared.SitePageData{Domain: template.URL(c.Domain), HomeURL: "/"},
76+ }
6477 }
6578
6679 var maxSize = uint64(50 * shared.MB)
......@@ -73,7 +86,7 @@ var cacheMaxBodySize = int64(0)
7386 // Needs to be small for caching files like _headers and _redirects.
7487 var maxSpecialFileSize = int64(5 * shared.KB)
7588
76-func NewPgsConfig(logger *slog.Logger, dbpool pgsdb.PgsDB, st storage.StorageServe, pubsub PicoPubsub) *PgsConfig {
89+func NewConfig(logger *slog.Logger, dbpool pgsdb.PgsDB, st storage.StorageServe, pubsub PicoPubsub) *Config {
7790 domain := shared.GetEnv("PGS_DOMAIN", "pgs.sh")
7891 port := shared.GetEnv("PGS_WEB_PORT", "3000")
7992 protocol := shared.GetEnv("PGS_PROTOCOL", "https")
......@@ -94,7 +107,7 @@ func NewPgsConfig(logger *slog.Logger, dbpool pgsdb.PgsDB, st storage.StorageSer
94107 sshHost := shared.GetEnv("PGS_SSH_HOST", "0.0.0.0")
95108 sshPort := shared.GetEnv("PGS_SSH_PORT", "2222")
96109
97- cfg := PgsConfig{
110+ cfg := Config{
98111 CacheTTL: cacheTTL,
99112 CacheMaxItems: cacheMaxItems,
100113 CacheMaxBodySize: cacheMaxBody,
+1, -1
......@@ -11,7 +11,7 @@ import (
1111
1212 const binRetentionDays = 14
1313
14-func BinCron(cfg *PgsConfig) {
14+func BinCron(cfg *Config) {
1515 logger := cfg.Logger
1616 storage := cfg.Storage
1717 db := cfg.DB
+7, -33
......@@ -1,43 +1,17 @@
11 package pgs
22
33 import (
4- "fmt"
5- "strings"
6-
74 pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
85 "github.com/picosh/pico/pkg/db"
96 )
107
11-func setFeatureLimits(ff *db.FeatureFlag, cfg *PgsConfig) {
12- ff.Data.StorageMax = ff.FindStorageMax(cfg.MaxSize)
13- ff.Data.FileMax = ff.FindFileMax(cfg.MaxAssetSize)
14- ff.Data.SpecialFileMax = ff.FindSpecialFileMax(cfg.MaxSpecialFileSize)
15-}
16-
17-func findFeatureFlag(dbpool pgsdb.PgsDB, cfg *PgsConfig, userID string) (*db.FeatureFlag, error) {
18- ff, err := dbpool.FindFeature(userID, "plus")
19- if err == nil {
20- if ff.IsValid() {
21- setFeatureLimits(ff, cfg)
22- return ff, nil
23- }
24- err = fmt.Errorf("ERROR: your pico+ has expired: https://blog.pico.sh/ann-038-pico-invite-system")
25- }
26-
27- ffPgs, pgsErr := dbpool.FindFeature(userID, "pgs")
28- if pgsErr == nil {
29- if ffPgs.IsValid() {
30- setFeatureLimits(ffPgs, cfg)
31- return ffPgs, nil
32- }
33- pgsErr = fmt.Errorf("ERROR: your pgs access has expired: https://blog.pico.sh/ann-038-pico-invite-system")
34- }
35-
36- if err != nil && strings.Contains(err.Error(), "expired") {
8+// findFeatureFlag returns the flag that lets a user upload to pgs, with
9+// pgs's limits filled in.
10+func findFeatureFlag(dbpool pgsdb.PgsDB, cfg *Config, userID string) (*db.FeatureFlag, error) {
11+ ff, err := db.FindAccess(dbpool, userID, "pgs")
12+ if err != nil {
3713 return nil, err
3814 }
39- if pgsErr != nil && strings.Contains(pgsErr.Error(), "expired") {
40- return nil, pgsErr
41- }
42- return nil, fmt.Errorf("ERROR: uploading to pgs requires an invitation or pico+: https://blog.pico.sh/ann-038-pico-invite-system")
15+ ff.SetLimits(cfg.MaxSize, cfg.MaxAssetSize, cfg.MaxSpecialFileSize)
16+ return ff, nil
4317 }
+3, -3
......@@ -26,7 +26,7 @@ type loginFormData struct {
2626 }
2727
2828 // serveLoginFormWithConfig renders and serves the login form using templates.
29-func serveLoginFormWithConfig(w http.ResponseWriter, r *http.Request, project *db.Project, cfg *PgsConfig, logger *slog.Logger) {
29+func serveLoginFormWithConfig(w http.ResponseWriter, r *http.Request, project *db.Project, cfg *Config, logger *slog.Logger) {
3030 // Determine error message from query params
3131 errorMsg := ""
3232 if r.URL.Query().Get("error") == "invalid" {
......@@ -40,7 +40,7 @@ func serveLoginFormWithConfig(w http.ResponseWriter, r *http.Request, project *d
4040
4141 w.WriteHeader(http.StatusForbidden)
4242
43- ts, err := renderTemplate(cfg, []string{cfg.StaticPath("html/login.page.tmpl")})
43+ ts, err := router.RenderTemplate(staticDir, []string{cfg.StaticPath("html/login.page.tmpl")})
4444 if err != nil {
4545 logger.Error("could not render login template", "err", err.Error())
4646 http.Error(w, "Server error", http.StatusInternalServerError)
......@@ -55,7 +55,7 @@ func serveLoginFormWithConfig(w http.ResponseWriter, r *http.Request, project *d
5555 }
5656
5757 // handleLogin processes the login form submission.
58-func handleLogin(w http.ResponseWriter, r *http.Request, cfg *PgsConfig) {
58+func handleLogin(w http.ResponseWriter, r *http.Request, cfg *Config) {
5959 if r.Method != http.MethodPost {
6060 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
6161 return
+1, -1
......@@ -57,7 +57,7 @@ func newRsyncEnv(t *testing.T, st storage.StorageServe) *rsyncEnv {
5757 bucket := storagetest.Bucket(t, st, shared.GetAssetBucketName(dbpool.Users[0].ID))
5858
5959 t.Setenv("PGS_SSH_PORT", "0")
60- cfg := NewPgsConfig(logger, dbpool, st, discardPubsub{})
60+ cfg := NewConfig(logger, dbpool, st, discardPubsub{})
6161 cfg.Domain = "pgs.sh"
6262 cfg.WebProtocol = "https"
6363 done := make(chan error)
+2, -2
......@@ -17,7 +17,7 @@ import (
1717 "github.com/picosh/pico/pkg/tunkit"
1818 )
1919
20-func createSshServer(cfg *PgsConfig, ctx context.Context, cacheClearingQueue chan string) (*pssh.SSHServer, error) {
20+func createSshServer(cfg *Config, ctx context.Context, cacheClearingQueue chan string) (*pssh.SSHServer, error) {
2121 host := shared.GetEnv("PGS_HOST", "0.0.0.0")
2222 port := shared.GetEnv("PGS_SSH_PORT", "2222")
2323 promPort := shared.GetEnv("PGS_PROM_PORT", "9222")
......@@ -67,7 +67,7 @@ func createSshServer(cfg *PgsConfig, ctx context.Context, cacheClearingQueue cha
6767 return server, err
6868 }
6969
70-func StartSshServer(cfg *PgsConfig, killCh chan error) {
70+func StartSshServer(cfg *Config, killCh chan error) {
7171 ctx, cancel := context.WithCancel(context.Background())
7272 defer cancel()
7373
+5, -5
......@@ -24,7 +24,7 @@ import (
2424 "golang.org/x/crypto/ssh"
2525 )
2626
27-func StartSshServerForTesting(cfg *PgsConfig, killCh chan error, readyCh chan *pssh.SSHServer) {
27+func StartSshServerForTesting(cfg *Config, killCh chan error, readyCh chan *pssh.SSHServer) {
2828 ctx, cancel := context.WithCancel(context.Background())
2929 defer func() {
3030 // Cancel is deferred to avoid being called in error path
......@@ -87,7 +87,7 @@ func TestSshServerSftp(t *testing.T) {
8787 // Use dynamic port for tests to avoid port conflicts
8888 _ = os.Setenv("PGS_SSH_PORT", "0")
8989
90- cfg := NewPgsConfig(logger, dbpool, st, pubsub)
90+ cfg := NewConfig(logger, dbpool, st, pubsub)
9191 done := make(chan error)
9292 readyCh := make(chan *pssh.SSHServer)
9393 prometheus.DefaultRegisterer = prometheus.NewRegistry()
......@@ -190,7 +190,7 @@ func TestSshServerRsync(t *testing.T) {
190190 // Use dynamic port for tests to avoid port conflicts
191191 _ = os.Setenv("PGS_SSH_PORT", "0")
192192
193- cfg := NewPgsConfig(logger, dbpool, st, pubsub)
193+ cfg := NewConfig(logger, dbpool, st, pubsub)
194194 done := make(chan error)
195195 readyCh := make(chan *pssh.SSHServer)
196196 prometheus.DefaultRegisterer = prometheus.NewRegistry()
......@@ -442,7 +442,7 @@ func GenerateUser() UserSSH {
442442 }
443443 }
444444
445-func WriteFileWithSftp(cfg *PgsConfig, conn *ssh.Client) (*os.FileInfo, error) {
445+func WriteFileWithSftp(cfg *Config, conn *ssh.Client) (*os.FileInfo, error) {
446446 // open an SFTP session over an existing ssh connection.
447447 client, err := sftp.NewClient(conn)
448448 if err != nil {
......@@ -475,7 +475,7 @@ func WriteFileWithSftp(cfg *PgsConfig, conn *ssh.Client) (*os.FileInfo, error) {
475475 return &fi, nil
476476 }
477477
478-func WriteFilesMultProjectsWithSftp(cfg *PgsConfig, conn *ssh.Client) (*os.FileInfo, error) {
478+func WriteFilesMultProjectsWithSftp(cfg *Config, conn *ssh.Client) (*os.FileInfo, error) {
479479 // open an SFTP session over an existing ssh connection.
480480 client, err := sftp.NewClient(conn)
481481 if err != nil {
+2, -4
......@@ -1,7 +1,6 @@
11 package pgs
22
33 import (
4- "context"
54 "net/http"
65 "strings"
76 "time"
......@@ -29,8 +28,7 @@ func tunnelPerm(proj *db.Project) bool {
2928 }
3029
3130 func (web *TunnelWebRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
32- ctx := r.Context()
33- ctx = context.WithValue(ctx, router.CtxSubdomainKey{}, web.subdomain)
31+ ctx := router.WithSubdomain(r.Context(), web.subdomain)
3432 web.UserRouter.ServeHTTP(w, r.WithContext(ctx))
3533 }
3634
......@@ -45,7 +43,7 @@ func getInfoFromUser(user string) (string, string) {
4543 return "", user
4644 }
4745
48-func CreateHttpHandler(cfg *PgsConfig) CtxHttpBridge {
46+func CreateHttpHandler(cfg *Config) CtxHttpBridge {
4947 return func(ctx *pssh.SSHServerConnSession) http.Handler {
5048 logger := cfg.Logger
5149 asUser, subdomain := getInfoFromUser(ctx.User())
+3, -3
......@@ -65,11 +65,11 @@ type FileData struct {
6565 }
6666
6767 type UploadAssetHandler struct {
68- Cfg *PgsConfig
68+ Cfg *Config
6969 CacheClearingQueue chan string
7070 }
7171
72-func NewUploadAssetHandler(cfg *PgsConfig, ch chan string, ctx context.Context) *UploadAssetHandler {
72+func NewUploadAssetHandler(cfg *Config, ch chan string, ctx context.Context) *UploadAssetHandler {
7373 go runCacheQueue(cfg, ctx)
7474 return &UploadAssetHandler{
7575 Cfg: cfg,
......@@ -513,7 +513,7 @@ func (h *UploadAssetHandler) writeAsset(s *pssh.SSHServerConnSession, reader io.
513513 // One message arrives per file that is written/deleted during uploads.
514514 // Repeated messages for the same site are grouped so that we only flush once
515515 // per site per 5 seconds.
516-func runCacheQueue(cfg *PgsConfig, ctx context.Context) {
516+func runCacheQueue(cfg *Config, ctx context.Context) {
517517 var pendingFlushes sync.Map
518518 tick := time.NewTicker(5 * time.Second)
519519 defer tick.Stop()
+5, -5
......@@ -59,7 +59,7 @@ func TestRsyncDeleteDirectory(t *testing.T) {
5959 _ = os.Setenv("PGS_SSH_PORT", "0")
6060 _ = os.Setenv("PGS_PROM_PORT", "0")
6161
62- cfg := NewPgsConfig(logger, dbpool, st, pubsub)
62+ cfg := NewConfig(logger, dbpool, st, pubsub)
6363 done := make(chan error)
6464 readyCh := make(chan *pssh.SSHServer)
6565 prometheus.DefaultRegisterer = prometheus.NewRegistry()
......@@ -250,7 +250,7 @@ func TestRsyncDeleteNestedEmptyDirectories(t *testing.T) {
250250 _ = os.Setenv("PGS_SSH_PORT", "0")
251251 _ = os.Setenv("PGS_PROM_PORT", "0")
252252
253- cfg := NewPgsConfig(logger, dbpool, st, pubsub)
253+ cfg := NewConfig(logger, dbpool, st, pubsub)
254254 done := make(chan error)
255255 readyCh := make(chan *pssh.SSHServer)
256256 prometheus.DefaultRegisterer = prometheus.NewRegistry()
......@@ -389,7 +389,7 @@ func setupPgsTestServer(t *testing.T, dbpool *pgsdb.MemoryDB) (string, func()) {
389389 _ = os.Setenv("PGS_SSH_PORT", "0")
390390 _ = os.Setenv("PGS_PROM_PORT", "0")
391391
392- cfg := NewPgsConfig(logger, dbpool, st, pubsub)
392+ cfg := NewConfig(logger, dbpool, st, pubsub)
393393 done := make(chan error)
394394 readyCh := make(chan *pssh.SSHServer)
395395 prometheus.DefaultRegisterer = prometheus.NewRegistry()
......@@ -550,7 +550,7 @@ func TestNoPlusFlagBlocksUpload(t *testing.T) {
550550
551551 func TestFindFeatureFlag(t *testing.T) {
552552 logger := slog.Default()
553- cfg := &PgsConfig{
553+ cfg := &Config{
554554 MaxSize: uint64(100 * shared.MB),
555555 MaxAssetSize: int64(10 * shared.MB),
556556 MaxSpecialFileSize: int64(5 * shared.KB),
......@@ -723,7 +723,7 @@ func TestSftpDirectories(t *testing.T) {
723723 t.Fatal(err)
724724 }
725725 _ = os.Setenv("PGS_SSH_PORT", "0")
726- cfg := NewPgsConfig(logger, dbpool, st, discardPubsub{})
726+ cfg := NewConfig(logger, dbpool, st, discardPubsub{})
727727 done := make(chan error)
728728 defer close(done)
729729 readyCh := make(chan *pssh.SSHServer)
+13, -101
......@@ -5,10 +5,8 @@ import (
55 "context"
66 "errors"
77 "fmt"
8- "html/template"
98 "log/slog"
109 "net/http"
11- "os"
1210 "path/filepath"
1311 "regexp"
1412 "strings"
......@@ -109,7 +107,7 @@ func (p *PromCacheMetrics) AddUpstreamRequest() {
109107 p.UpstreamReq.Add(1)
110108 }
111109
112-func NewPgsHttpCache(cfg *PgsConfig, upstream http.Handler) (*httpcache.HttpCache, *SiteCache) {
110+func NewPgsHttpCache(cfg *Config, upstream http.Handler) (*httpcache.HttpCache, *SiteCache) {
113111 ttl := cfg.CacheTTL
114112 metrics := NewPromCacheMetrics(cfg.Logger, prometheus.DefaultRegisterer)
115113 cache := NewSiteCache(cfg.CacheMaxItems, ttl, metrics.EvictCacheItem)
......@@ -135,7 +133,7 @@ func NewPgsHttpCache(cfg *PgsConfig, upstream http.Handler) (*httpcache.HttpCach
135133 return httpCache, cache
136134 }
137135
138-func StartApiServer(cfg *PgsConfig) {
136+func StartApiServer(cfg *Config) {
139137 ctx := context.Background()
140138
141139 router := NewWebRouter(cfg)
......@@ -158,7 +156,7 @@ func StartApiServer(cfg *PgsConfig) {
158156 type HasPerm = func(proj *db.Project) bool
159157
160158 type WebRouter struct {
161- Cfg *PgsConfig
159+ Cfg *Config
162160 RootRouter *http.ServeMux
163161 UserRouter *http.ServeMux
164162 RedirectsCache *expirable.LRU[string, []*RedirectRule]
......@@ -178,11 +176,11 @@ func (s *siteInfo) hasPicoPlus() bool {
178176 return s.plus != nil && s.plus.ExpiresAt.After(time.Now())
179177 }
180178
181-func NewWebRouter(cfg *PgsConfig) *WebRouter {
179+func NewWebRouter(cfg *Config) *WebRouter {
182180 return newWebRouter(cfg)
183181 }
184182
185-func newWebRouter(cfg *PgsConfig) *WebRouter {
183+func newWebRouter(cfg *Config) *WebRouter {
186184 router := &WebRouter{
187185 Cfg: cfg,
188186 RedirectsCache: expirable.NewLRU[string, []*RedirectRule](2048, nil, shared.CacheTimeout),
......@@ -194,23 +192,12 @@ func newWebRouter(cfg *PgsConfig) *WebRouter {
194192 }
195193
196194 func (web *WebRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
197- subdomain := router.GetSubdomainFromRequest(r, web.Cfg.Domain, web.Cfg.TxtPrefix)
198195 if web.RootRouter == nil || web.UserRouter == nil {
199196 web.Cfg.Logger.Error("routers not initialized")
200197 http.Error(w, "routers not initialized", http.StatusInternalServerError)
201198 return
202199 }
203-
204- var mux *http.ServeMux
205- if subdomain == "" {
206- mux = web.RootRouter
207- } else {
208- mux = web.UserRouter
209- }
210-
211- ctx := r.Context()
212- ctx = context.WithValue(ctx, router.CtxSubdomainKey{}, subdomain)
213- mux.ServeHTTP(w, r.WithContext(ctx))
200+ router.NewHostRouter(web.Cfg.Domain, web.Cfg.TxtPrefix, web.RootRouter, web.UserRouter).ServeHTTP(w, r)
214201 }
215202
216203 // clearSiteCache drops what is cached about a site: its database records and
......@@ -272,14 +259,15 @@ func (web *WebRouter) initRouters() {
272259 w.Header().Set("cache-control", "no-store")
273260 promhttp.Handler().ServeHTTP(w, r)
274261 })
275- rootRouter.Handle("GET /main.css", web.serveFile("main.css", "text/css"))
276- rootRouter.Handle("GET /favicon-16x16.png", web.serveFile("favicon-16x16.png", "image/png"))
277- rootRouter.Handle("GET /favicon.ico", web.serveFile("favicon.ico", "image/x-icon"))
278- rootRouter.Handle("GET /robots.txt", web.serveFile("robots.txt", "text/plain"))
262+ files := web.Cfg.Static()
263+ rootRouter.Handle("GET /main.css", files.File("main.css", "text/css"))
264+ rootRouter.Handle("GET /favicon-16x16.png", files.File("favicon-16x16.png", "image/png"))
265+ rootRouter.Handle("GET /favicon.ico", files.File("favicon.ico", "image/x-icon"))
266+ rootRouter.Handle("GET /robots.txt", files.File("robots.txt", "text/plain"))
279267
280268 rootRouter.Handle("GET /rss/updated", web.createRssHandler("updated_at"))
281269 rootRouter.Handle("GET /rss", web.createRssHandler("created_at"))
282- rootRouter.Handle("GET /{$}", web.createPageHandler("html/marketing.page.tmpl"))
270+ rootRouter.Handle("GET /{$}", files.Page("html/marketing.page.tmpl"))
283271 web.RootRouter = rootRouter
284272
285273 // subdomain or custom domains
......@@ -291,82 +279,6 @@ func (web *WebRouter) initRouters() {
291279 web.UserRouter = userRouter
292280 }
293281
294-func (web *WebRouter) serveFile(file string, contentType string) http.HandlerFunc {
295- return func(w http.ResponseWriter, r *http.Request) {
296- logger := web.Cfg.Logger
297- cfg := web.Cfg
298-
299- contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
300- if err != nil {
301- logger.Error(
302- "could not read file",
303- "fname", file,
304- "err", err.Error(),
305- )
306- http.Error(w, "file not found", 404)
307- }
308-
309- w.Header().Add("Content-Type", contentType)
310-
311- _, err = w.Write(contents)
312- if err != nil {
313- logger.Error(
314- "could not write http response",
315- "file", file,
316- "err", err.Error(),
317- )
318- }
319- }
320-}
321-
322-func renderTemplate(cfg *PgsConfig, templates []string) (*template.Template, error) {
323- files := make([]string, len(templates))
324- copy(files, templates)
325- files = append(
326- files,
327- cfg.StaticPath("html/footer.partial.tmpl"),
328- cfg.StaticPath("html/marketing-footer.partial.tmpl"),
329- cfg.StaticPath("html/base.layout.tmpl"),
330- )
331-
332- ts, err := template.New("base").ParseFiles(files...)
333- if err != nil {
334- return nil, err
335- }
336- return ts, nil
337-}
338-
339-func (web *WebRouter) createPageHandler(fname string) http.HandlerFunc {
340- return func(w http.ResponseWriter, r *http.Request) {
341- logger := web.Cfg.Logger
342- cfg := web.Cfg
343- ts, err := renderTemplate(cfg, []string{cfg.StaticPath(fname)})
344-
345- if err != nil {
346- logger.Error(
347- "could not render template",
348- "fname", fname,
349- "err", err.Error(),
350- )
351- http.Error(w, err.Error(), http.StatusInternalServerError)
352- return
353- }
354-
355- data := shared.PageData{
356- Site: shared.SitePageData{Domain: template.URL(cfg.Domain), HomeURL: "/"},
357- }
358- err = ts.Execute(w, data)
359- if err != nil {
360- logger.Error(
361- "could not execute template",
362- "fname", fname,
363- "err", err.Error(),
364- )
365- http.Error(w, err.Error(), http.StatusInternalServerError)
366- }
367- }
368-}
369-
370282 func (web *WebRouter) checkHandler(w http.ResponseWriter, r *http.Request) {
371283 dbpool := web.Cfg.DB
372284 cfg := web.Cfg
......@@ -430,7 +342,7 @@ func (web *WebRouter) checkHandler(w http.ResponseWriter, r *http.Request) {
430342 // CacheMgmt applies the purges published on the cache-drain pipe. onPurge,
431343 // when set, clears whatever else is cached about the site; it runs first so
432344 // the http cache isn't refilled from stale records.
433-func CacheMgmt(ctx context.Context, cfg *PgsConfig, cache *SiteCache, onPurge func(site string)) {
345+func CacheMgmt(ctx context.Context, cfg *Config, cache *SiteCache, onPurge func(site string)) {
434346 cfg.Logger.Info("cache mgmt initiated")
435347 for {
436348 scanner := bufio.NewScanner(cfg.Pubsub)
+2, -2
......@@ -105,13 +105,13 @@ func CreateSubCacheDrain(ctx context.Context, logger *slog.Logger) *pipe.Reconne
105105 // cached assets for a given subdomain are grouped under a single key (which is
106106 // separate from the "GET-https-example.com-/path" key used for serving files
107107 // from the cache).
108-func purgeCache(cfg *PgsConfig, writer io.Writer, surrogate string) error {
108+func purgeCache(cfg *Config, writer io.Writer, surrogate string) error {
109109 cfg.Logger.Info("purging cache", "surrogate", surrogate)
110110 _, err := writer.Write([]byte(surrogate + "\n"))
111111 return err
112112 }
113113
114-func purgeAllCache(cfg *PgsConfig, writer io.Writer) error {
114+func purgeAllCache(cfg *Config, writer io.Writer) error {
115115 return purgeCache(cfg, writer, "*")
116116 }
117117
+6, -6
......@@ -461,7 +461,7 @@ func TestApiBasic(t *testing.T) {
461461 defer func() {
462462 _ = pubsub.Close()
463463 }()
464- cfg := NewPgsConfig(logger, dbpool, st, pubsub)
464+ cfg := NewConfig(logger, dbpool, st, pubsub)
465465 cfg.Domain = "pgs.test"
466466 router := NewWebRouter(cfg)
467467 router.ServeHTTP(responseRecorder, request)
......@@ -543,7 +543,7 @@ func TestApiHttpPassNotCached(t *testing.T) {
543543 defer func() {
544544 _ = pubsub.Close()
545545 }()
546- cfg := NewPgsConfig(logger, dbpool, st, pubsub)
546+ cfg := NewConfig(logger, dbpool, st, pubsub)
547547 cfg.Domain = "pgs.test"
548548 router := NewWebRouter(cfg)
549549
......@@ -662,7 +662,7 @@ func TestDirectoryListing(t *testing.T) {
662662 defer func() {
663663 _ = pubsub.Close()
664664 }()
665- cfg := NewPgsConfig(logger, dbpool, st, pubsub)
665+ cfg := NewConfig(logger, dbpool, st, pubsub)
666666 cfg.Domain = "pgs.test"
667667 router := NewWebRouter(cfg)
668668 router.ServeHTTP(responseRecorder, request)
......@@ -754,7 +754,7 @@ func TestDirectoryListing(t *testing.T) {
754754 // defer func() {
755755 // _ = pubsub.Close()
756756 // }()
757-// cfg := NewPgsConfig(logger, dbpool, st, pubsub)
757+// cfg := NewConfig(logger, dbpool, st, pubsub)
758758 // cfg.Domain = "pgs.test"
759759 // router := NewWebRouter(cfg)
760760 // router.ServeHTTP(responseRecorder, request)
......@@ -795,7 +795,7 @@ func TestRulesCacheClearedByUploadPurge(t *testing.T) {
795795 if err != nil {
796796 t.Fatal(err)
797797 }
798- cfg := NewPgsConfig(logger, dbpool, st, discardPubsub{})
798+ cfg := NewConfig(logger, dbpool, st, discardPubsub{})
799799 cfg.Domain = "pgs.test"
800800 router := newWebRouter(cfg)
801801
......@@ -853,7 +853,7 @@ func TestSiteCache(t *testing.T) {
853853 if err != nil {
854854 t.Fatal(err)
855855 }
856- cfg := NewPgsConfig(logger, dbpool, st, discardPubsub{})
856+ cfg := NewConfig(logger, dbpool, st, discardPubsub{})
857857 cfg.Domain = "pgs.test"
858858 router := newWebRouter(cfg)
859859
+10, -2
......@@ -1,17 +1,25 @@
11 package pico
22
33 import (
4+ "log/slog"
45 "strings"
56
67 "github.com/picosh/pico/pkg/shared"
78 )
89
9-func NewConfigSite(service string) *shared.ConfigSite {
10+type Config struct {
11+ DbURL string
12+ Space string
13+ TunsSecret string
14+ Logger *slog.Logger
15+}
16+
17+func NewConfig(service string) *Config {
1018 dbURL := shared.GetEnv("DATABASE_URL", "")
1119 tuns := shared.GetEnv("TUNS_CONSOLE_SECRET", "")
1220 withPipe := strings.ToLower(shared.GetEnv("PICO_PIPE_ENABLED", "true")) == "true"
1321
14- return &shared.ConfigSite{
22+ return &Config{
1523 DbURL: dbURL,
1624 Space: "pico",
1725 Logger: shared.CreateLogger(service, withPipe),
+2, -2
......@@ -20,10 +20,10 @@ import (
2020
2121 type UploadHandler struct {
2222 DBPool db.DB
23- Cfg *shared.ConfigSite
23+ Cfg *Config
2424 }
2525
26-func NewUploadHandler(dbpool db.DB, cfg *shared.ConfigSite) *UploadHandler {
26+func NewUploadHandler(dbpool db.DB, cfg *Config) *UploadHandler {
2727 return &UploadHandler{
2828 DBPool: dbpool,
2929 Cfg: cfg,
+6, -6
......@@ -43,7 +43,7 @@ func StartSshServer() {
4343 host := shared.GetEnv("PICO_HOST", "0.0.0.0")
4444 port := shared.GetEnv("PICO_SSH_PORT", "2222")
4545 promPort := shared.GetEnv("PICO_PROM_PORT", "9222")
46- cfg := NewConfigSite(appName)
46+ cfg := NewConfig(appName)
4747 logger := cfg.Logger
4848
4949 ctx, cancel := context.WithCancel(context.Background())
......@@ -99,11 +99,11 @@ func StartSshServer() {
9999 func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
100100 return func(sesh *pssh.SSHServerConnSession) error {
101101 shrd := &tui.SharedModel{
102- Session: sesh,
103- Cfg: cfg,
104- Dbpool: handler.DBPool,
105- PgsDB: pgsDB,
106- Logger: pssh.GetLogger(sesh),
102+ Session: sesh,
103+ TunsSecret: cfg.TunsSecret,
104+ Dbpool: handler.DBPool,
105+ PgsDB: pgsDB,
106+ Logger: pssh.GetLogger(sesh),
107107 }
108108 return pssh.PtyMdw(createTui(shrd), 200*time.Millisecond)(next)(sesh)
109109 }
+12, -11
......@@ -397,7 +397,7 @@ func (web *webRouter) handlePipe() http.HandlerFunc {
397397 }
398398 }
399399
400-func rssHandler(cfg *shared.ConfigSite, dbpool DB) http.HandlerFunc {
400+func rssHandler(cfg *Config, dbpool DB) http.HandlerFunc {
401401 return func(w http.ResponseWriter, r *http.Request) {
402402 apiToken, _ := url.PathUnescape(r.PathValue("token"))
403403 user, err := dbpool.FindUserByToken(apiToken)
......@@ -435,17 +435,18 @@ func rssHandler(cfg *shared.ConfigSite, dbpool DB) http.HandlerFunc {
435435 }
436436
437437 type webRouter struct {
438- cfg *shared.ConfigSite
438+ cfg *Config
439439 db DB
440440 }
441441
442442 func (web *webRouter) static(mux *http.ServeMux) {
443443 cfg := web.cfg
444- mux.HandleFunc("GET /favicon-16x16.png", router.ServeFile(cfg, "favicon-16x16.png", "image/png"))
445- mux.HandleFunc("GET /apple-touch-icon.png", router.ServeFile(cfg, "apple-touch-icon.png", "image/png"))
446- mux.HandleFunc("GET /favicon.ico", router.ServeFile(cfg, "favicon.ico", "image/x-icon"))
447- mux.HandleFunc("GET /robots.txt", router.ServeFile(cfg, "robots.txt", "text/plain"))
448- mux.HandleFunc("GET /anim.js", router.ServeFile(cfg, "anim.js", "text/javascript"))
444+ files := cfg.Static()
445+ mux.HandleFunc("GET /favicon-16x16.png", files.File("favicon-16x16.png", "image/png"))
446+ mux.HandleFunc("GET /apple-touch-icon.png", files.File("apple-touch-icon.png", "image/png"))
447+ mux.HandleFunc("GET /favicon.ico", files.File("favicon.ico", "image/x-icon"))
448+ mux.HandleFunc("GET /robots.txt", files.File("robots.txt", "text/plain"))
449+ mux.HandleFunc("GET /anim.js", files.File("anim.js", "text/javascript"))
449450 if cfg.Debug {
450451 router.CreatePProfRoutesMux(mux)
451452 }
......@@ -453,8 +454,8 @@ func (web *webRouter) static(mux *http.ServeMux) {
453454
454455 func (web *webRouter) mainMux() *http.ServeMux {
455456 mux := http.NewServeMux()
456- mux.HandleFunc("GET /{$}", router.CreatePageHandler(web.cfg, "html/marketing.page.tmpl"))
457- mux.HandleFunc("GET /check", router.CheckHandler(web.cfg, web.db))
457+ mux.HandleFunc("GET /{$}", web.cfg.Static().Page("html/marketing.page.tmpl"))
458+ mux.HandleFunc("GET /check", router.CheckHandler(web.cfg.Domain, web.cfg.Space, web.db))
458459 mux.HandleFunc("GET /rss/{token...}", rssHandler(web.cfg, web.db))
459460 mux.Handle("GET /_metrics", promhttp.Handler())
460461
......@@ -479,7 +480,7 @@ func (web *webRouter) sitesMux() *http.ServeMux {
479480 }
480481
481482 func StartApiServer() {
482- cfg := NewConfigSite("pipe-web")
483+ cfg := NewConfig("pipe-web")
483484 db := NewDB(postgres.NewDB(cfg.DbURL, cfg.Logger))
484485 defer func() {
485486 _ = db.Close()
......@@ -512,7 +513,7 @@ func StartApiServer() {
512513 }()
513514
514515 web := &webRouter{cfg: cfg, db: db}
515- handler := router.NewHostRouter(cfg, web.mainMux(), web.sitesMux())
516+ handler := router.NewHostRouter(cfg.Domain, cfg.Space, web.mainMux(), web.sitesMux())
516517
517518 portStr := fmt.Sprintf(":%s", cfg.Port)
518519 logger.Info(
+2, -2
......@@ -12,10 +12,10 @@ import (
1212 func TestRoutes(t *testing.T) {
1313 t.Chdir("../../..")
1414 t.Setenv("PICO_PIPE_ENABLED", "false")
15- cfg := NewConfigSite("pipe-test")
15+ cfg := NewConfig("pipe-test")
1616 cfg.Logger = slog.New(slog.DiscardHandler)
1717 web := &webRouter{cfg: cfg, db: NewTestDB(cfg.Logger)}
18- handler := router.NewHostRouter(cfg, web.mainMux(), web.sitesMux())
18+ handler := router.NewHostRouter(cfg.Domain, cfg.Space, web.mainMux(), web.sitesMux())
1919
2020 site := "https://tester." + cfg.Domain
2121 main := "https://" + cfg.Domain
+3, -4
......@@ -21,7 +21,6 @@ import (
2121 "github.com/picosh/pico/pkg/db"
2222 "github.com/picosh/pico/pkg/pssh"
2323 psub "github.com/picosh/pico/pkg/pubsub"
24- "github.com/picosh/pico/pkg/shared"
2524 gossh "golang.org/x/crypto/ssh"
2625 )
2726
......@@ -188,7 +187,7 @@ type CliHandler struct {
188187 DBPool DB
189188 Logger *slog.Logger
190189 PubSub psub.PubSub
191- Cfg *shared.ConfigSite
190+ Cfg *Config
192191 Waiters *syncmap.Map[string, []string]
193192 Access *syncmap.Map[string, []string]
194193 }
......@@ -207,7 +206,7 @@ type CliCmd struct {
207206 user *db.User
208207 }
209208
210-func help(cfg *shared.ConfigSite, sesh *pssh.SSHServerConnSession) {
209+func help(cfg *Config, sesh *pssh.SSHServerConnSession) {
211210 data := fmt.Sprintf(`Command: ssh %s <command> [args...]
212211
213212 The simplest authenticated pubsub system. Send messages through
......@@ -1153,7 +1152,7 @@ func (handler *CliHandler) pipe(cmd *CliCmd, topic string, clientID string) erro
11531152 return nil
11541153 }
11551154
1156-func toSshCmd(cfg *shared.ConfigSite) string {
1155+func toSshCmd(cfg *Config) string {
11571156 port := ""
11581157 if cfg.PortOverride != "22" {
11591158 port = fmt.Sprintf("-p %s ", cfg.PortOverride)
+31, -2
......@@ -1,19 +1,37 @@
11 package pipe
22
33 import (
4+ "html/template"
5+ "log/slog"
46 "strings"
57
68 "github.com/picosh/pico/pkg/shared"
9+ "github.com/picosh/pico/pkg/shared/router"
710 )
811
9-func NewConfigSite(service string) *shared.ConfigSite {
12+// staticDir holds pipe's templates and public files.
13+const staticDir = "pkg/apps/pipe"
14+
15+type Config struct {
16+ Debug bool
17+ Domain string
18+ Port string
19+ // PortOverride is the ssh port users connect to, when it isn't 22.
20+ PortOverride string
21+ Protocol string
22+ DbURL string
23+ Space string
24+ Logger *slog.Logger
25+}
26+
27+func NewConfig(service string) *Config {
1028 domain := shared.GetEnv("PIPE_DOMAIN", "pipe.pico.sh")
1129 port := shared.GetEnv("PIPE_WEB_PORT", "3000")
1230 dbURL := shared.GetEnv("DATABASE_URL", "")
1331 protocol := shared.GetEnv("PIPE_PROTOCOL", "https")
1432 withPipe := strings.ToLower(shared.GetEnv("PICO_PIPE_ENABLED", "true")) == "true"
1533
16- return &shared.ConfigSite{
34+ return &Config{
1735 Domain: domain,
1836 Port: port,
1937 Protocol: protocol,
......@@ -22,3 +40,14 @@ func NewConfigSite(service string) *shared.ConfigSite {
2240 Space: "pipe",
2341 }
2442 }
43+
44+func (c *Config) Static() router.Static {
45+ return router.Static{
46+ Dir: staticDir,
47+ Logger: c.Logger,
48+ Site: shared.SitePageData{
49+ Domain: template.URL(c.Domain),
50+ HomeURL: template.URL("//" + c.Domain),
51+ },
52+ }
53+}
+1, -1
......@@ -21,7 +21,7 @@ func StartSshServer() {
2121 port := shared.GetEnv("PIPE_SSH_PORT", "2222")
2222 portOverride := shared.GetEnv("PIPE_SSH_PORT_OVERRIDE", port)
2323 promPort := shared.GetEnv("PIPE_PROM_PORT", "9222")
24- cfg := NewConfigSite(appName)
24+ cfg := NewConfig(appName)
2525 logger := cfg.Logger
2626
2727 ctx, cancel := context.WithCancel(context.Background())
+66, -22
......@@ -8,6 +8,7 @@ import (
88 "fmt"
99 "io"
1010 "log/slog"
11+ "net"
1112 "os"
1213 "strings"
1314 "sync"
......@@ -237,7 +238,7 @@ func (t *TestDB) FindPipeMonitorHistory(monitorID string, from, to time.Time) ([
237238 }
238239
239240 type TestSSHServer struct {
240- Cfg *shared.ConfigSite
241+ Cfg *Config
241242 DBPool *TestDB
242243 PipeHandler *CliHandler
243244 Cancel context.CancelFunc
......@@ -254,7 +255,7 @@ func NewTestSSHServer(t *testing.T) *TestSSHServer {
254255
255256 dbpool := NewTestDB(logger)
256257
257- cfg := &shared.ConfigSite{
258+ cfg := &Config{
258259 Domain: "pipe.test",
259260 Port: "2225",
260261 PortOverride: "2225",
......@@ -316,7 +317,7 @@ func NewTestSSHServer(t *testing.T) *TestSSHServer {
316317 }
317318 }()
318319
319- time.Sleep(100 * time.Millisecond)
320+ waitForPort(t, cfg.Port, true)
320321
321322 return &TestSSHServer{
322323 Cfg: cfg,
......@@ -328,7 +329,50 @@ func NewTestSSHServer(t *testing.T) *TestSSHServer {
328329
329330 func (s *TestSSHServer) Shutdown() {
330331 s.Cancel()
331- time.Sleep(10 * time.Millisecond)
332+ waitForPort(nil, s.Cfg.Port, false)
333+}
334+
335+// waitForPort waits until the test server's port accepts connections, or
336+// until it stops accepting them so the next test can listen on it.
337+func waitForPort(t *testing.T, port string, open bool) {
338+ deadline := time.Now().Add(5 * time.Second)
339+ for time.Now().Before(deadline) {
340+ conn, err := net.DialTimeout("tcp", "localhost:"+port, 100*time.Millisecond)
341+ if err == nil {
342+ _ = conn.Close()
343+ }
344+ if (err == nil) == open {
345+ return
346+ }
347+ time.Sleep(5 * time.Millisecond)
348+ }
349+ if t != nil {
350+ t.Fatalf("port %s open = %v, want %v", port, !open, open)
351+ }
352+}
353+
354+// waitForClients waits until n subscribers and pipes have joined their
355+// topics, so a message published next reaches them.
356+func (s *TestSSHServer) waitForClients(t *testing.T, n int) {
357+ t.Helper()
358+ ps := s.PipeHandler.PubSub.(*psub.Multicast)
359+ deadline := time.Now().Add(5 * time.Second)
360+ for {
361+ joined := 0
362+ for range ps.GetSubs() {
363+ joined++
364+ }
365+ for range ps.GetPipes() {
366+ joined++
367+ }
368+ if joined >= n {
369+ return
370+ }
371+ if time.Now().After(deadline) {
372+ t.Fatalf("%d of %d subscribers and pipes joined", joined, n)
373+ }
374+ time.Sleep(5 * time.Millisecond)
375+ }
332376 }
333377
334378 type UserSSH struct {
......@@ -539,7 +583,7 @@ func TestPubSub_BasicFlow(t *testing.T) {
539583 t.Fatalf("failed to start sub: %v", err)
540584 }
541585
542- time.Sleep(100 * time.Millisecond)
586+ server.waitForClients(t, 1)
543587
544588 testMessage := "hello from pub"
545589 _, err = user.RunCommandWithStdin(pubClient, "pub testtopic -c", testMessage)
......@@ -595,7 +639,7 @@ func TestPubSub_PublicTopic(t *testing.T) {
595639 t.Fatalf("failed to start sub: %v", err)
596640 }
597641
598- time.Sleep(100 * time.Millisecond)
642+ server.waitForClients(t, 1)
599643
600644 testMessage := "public message"
601645 _, err = alice.RunCommandWithStdin(pubClient, "pub publictopic -p -c", testMessage)
......@@ -656,7 +700,7 @@ func TestPipe_Bidirectional(t *testing.T) {
656700 t.Fatalf("failed to start alice pipe: %v", err)
657701 }
658702
659- time.Sleep(100 * time.Millisecond)
703+ server.waitForClients(t, 1)
660704
661705 bobSession, err := bobClient.NewSession()
662706 if err != nil {
......@@ -678,7 +722,7 @@ func TestPipe_Bidirectional(t *testing.T) {
678722 t.Fatalf("failed to start bob pipe: %v", err)
679723 }
680724
681- time.Sleep(100 * time.Millisecond)
725+ server.waitForClients(t, 2)
682726
683727 aliceMsg := "hello from alice\n"
684728 _, err = aliceStdin.Write([]byte(aliceMsg))
......@@ -798,7 +842,7 @@ func TestAccessControl_AllowedUserViaFullPath(t *testing.T) {
798842 t.Fatalf("failed to start alice sub: %v", err)
799843 }
800844
801- time.Sleep(100 * time.Millisecond)
845+ server.waitForClients(t, 1)
802846
803847 bobClient, err := bob.NewClient()
804848 if err != nil {
......@@ -908,7 +952,7 @@ func TestPubSub_BlockingWaitsForSubscriber(t *testing.T) {
908952 t.Fatalf("failed to start sub: %v", err)
909953 }
910954
911- time.Sleep(100 * time.Millisecond)
955+ server.waitForClients(t, 1)
912956
913957 // Now send the message
914958 testMessage := "blocking message"
......@@ -1036,7 +1080,7 @@ func TestSub_WaitsForPublisher(t *testing.T) {
10361080 }
10371081
10381082 // Subscriber is now waiting - give it a moment
1039- time.Sleep(100 * time.Millisecond)
1083+ server.waitForClients(t, 1)
10401084
10411085 // Now publish - subscriber should receive it
10421086 testMessage := "delayed publish"
......@@ -1097,7 +1141,7 @@ func TestSub_KeepAliveReceivesMultipleMessages(t *testing.T) {
10971141 t.Fatalf("failed to start sub: %v", err)
10981142 }
10991143
1100- time.Sleep(100 * time.Millisecond)
1144+ server.waitForClients(t, 1)
11011145
11021146 // Send first message
11031147 msg1 := "first message\n"
......@@ -1160,7 +1204,7 @@ func TestSub_WithoutKeepAliveExitsAfterPublisher(t *testing.T) {
11601204 t.Fatalf("failed to start sub: %v", err)
11611205 }
11621206
1163- time.Sleep(100 * time.Millisecond)
1207+ server.waitForClients(t, 1)
11641208
11651209 // Publish a message
11661210 testMessage := "single message"
......@@ -1227,7 +1271,7 @@ func TestPub_EmptyMessage(t *testing.T) {
12271271 t.Fatalf("failed to start sub: %v", err)
12281272 }
12291273
1230- time.Sleep(100 * time.Millisecond)
1274+ server.waitForClients(t, 1)
12311275
12321276 // Publish with -e flag (empty message) - should not require stdin
12331277 output, err := user.RunCommand(pubClient, "pub emptytopic -e -c")
......@@ -1291,7 +1335,7 @@ func TestPipe_AccessControl(t *testing.T) {
12911335 t.Fatalf("failed to start alice pipe: %v", err)
12921336 }
12931337
1294- time.Sleep(100 * time.Millisecond)
1338+ server.waitForClients(t, 1)
12951339
12961340 // Bob joins the pipe using alice's namespace
12971341 bobSession, err := bobClient.NewSession()
......@@ -1314,7 +1358,7 @@ func TestPipe_AccessControl(t *testing.T) {
13141358 t.Fatalf("failed to start bob pipe: %v", err)
13151359 }
13161360
1317- time.Sleep(100 * time.Millisecond)
1361+ server.waitForClients(t, 2)
13181362
13191363 // Alice sends message to bob
13201364 aliceMsg := "hello bob\n"
......@@ -1377,7 +1421,7 @@ func TestPipe_Replay(t *testing.T) {
13771421 t.Fatalf("failed to start pipe: %v", err)
13781422 }
13791423
1380- time.Sleep(100 * time.Millisecond)
1424+ server.waitForClients(t, 1)
13811425
13821426 // Send a message - with -r flag, should receive it back
13831427 testMsg := "echo back\n"
......@@ -1431,7 +1475,7 @@ func TestAccessControl_UnauthorizedUserDenied(t *testing.T) {
14311475 t.Fatalf("failed to start alice sub: %v", err)
14321476 }
14331477
1434- time.Sleep(100 * time.Millisecond)
1478+ server.waitForClients(t, 1)
14351479
14361480 // Charlie tries to publish to alice's restricted topic - should be denied
14371481 output, err := charlie.RunCommandWithStdin(charlieClient, "pub alice/restrictedtopic -c", "unauthorized message")
......@@ -1524,7 +1568,7 @@ func TestPubSub_MultipleSubscribers(t *testing.T) {
15241568 t.Fatalf("failed to start sub3: %v", err)
15251569 }
15261570
1527- time.Sleep(100 * time.Millisecond)
1571+ server.waitForClients(t, 3)
15281572
15291573 // Publish a single message
15301574 testMessage := "broadcast message"
......@@ -1995,7 +2039,7 @@ func TestPub_UpdatesMonitorLastPing(t *testing.T) {
19952039 t.Fatalf("failed to start sub: %v", err)
19962040 }
19972041
1998- time.Sleep(100 * time.Millisecond)
2042+ server.waitForClients(t, 1)
19992043
20002044 // Publish to the monitored topic
20012045 _, err = user.RunCommandWithStdin(pubClient, "pub ping-test -c", "health check")
......@@ -2093,7 +2137,7 @@ func TestStatus_PingResetsWindow(t *testing.T) {
20932137 t.Fatalf("failed to start sub: %v", err)
20942138 }
20952139
2096- time.Sleep(100 * time.Millisecond)
2140+ server.waitForClients(t, 1)
20972141
20982142 // Pub to trigger monitor update
20992143 _, err = user.RunCommandWithStdin(client, "pub reset-test -c", "ping")
......@@ -2154,7 +2198,7 @@ func TestStatus_HealthyImmediatelyAfterPing(t *testing.T) {
21542198 t.Fatalf("failed to start sub: %v", err)
21552199 }
21562200
2157- time.Sleep(100 * time.Millisecond)
2201+ server.waitForClients(t, 1)
21582202
21592203 // Pub to trigger ping
21602204 pubClient, err := user.NewClient()
+47, -48
......@@ -119,7 +119,7 @@ func GetBlogName(username string) string {
119119 }
120120
121121 func (web *webRouter) blogStyleHandler(w http.ResponseWriter, r *http.Request) {
122- username := router.GetUsernameFromRequest(r, web.cfg)
122+ username := router.GetUsernameFromRequest(r)
123123 dbpool := web.db
124124 logger := web.cfg.Logger
125125 cfg := web.cfg
......@@ -149,7 +149,7 @@ func (web *webRouter) blogStyleHandler(w http.ResponseWriter, r *http.Request) {
149149 }
150150
151151 func (web *webRouter) blogHandler(w http.ResponseWriter, r *http.Request) {
152- username := router.GetUsernameFromRequest(r, web.cfg)
152+ username := router.GetUsernameFromRequest(r)
153153 dbpool := web.db
154154 logger := web.cfg.Logger
155155 cfg := web.cfg
......@@ -179,12 +179,12 @@ func (web *webRouter) blogHandler(w http.ResponseWriter, r *http.Request) {
179179 return
180180 }
181181
182- ts, err := router.RenderTemplate(cfg, []string{
182+ ts, err := router.RenderTemplate(staticDir, []string{
183183 cfg.StaticPath("html/blog-default.partial.tmpl"),
184184 cfg.StaticPath("html/blog.page.tmpl"),
185185 })
186186
187- curl := shared.CreateURLFromRequest(cfg, r)
187+ curl := shared.CreateURLFromRequest(cfg.Protocol, cfg.Domain, r)
188188
189189 if err != nil {
190190 logger.Error("render template", "err", err.Error())
......@@ -218,9 +218,7 @@ func (web *webRouter) blogHandler(w http.ResponseWriter, r *http.Request) {
218218 u, _ := url.Parse(nav.URL)
219219 finURL := nav.URL
220220 if !u.IsAbs() {
221- finURL = cfg.FullPostURL(
222- curl,
223- readme.Username,
221+ finURL = curl.FullPostURL(readme.Username,
224222 nav.URL,
225223 )
226224 }
......@@ -245,8 +243,8 @@ func (web *webRouter) blogHandler(w http.ResponseWriter, r *http.Request) {
245243 postCollection := make([]PostItemData, 0, len(posts))
246244 for _, post := range posts {
247245 p := PostItemData{
248- URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
249- BlogURL: template.URL(cfg.FullBlogURL(curl, post.Username)),
246+ URL: template.URL(curl.FullPostURL(post.Username, post.Slug)),
247+ BlogURL: template.URL(curl.FullBlogURL(post.Username)),
250248 Title: shared.FilenameToTitle(post.Filename, post.Title),
251249 PublishAt: post.PublishAt.Format(time.DateOnly),
252250 PublishAtISO: post.PublishAt.Format(time.RFC3339),
......@@ -257,16 +255,16 @@ func (web *webRouter) blogHandler(w http.ResponseWriter, r *http.Request) {
257255 }
258256
259257 data := BlogPageData{
260- Site: *cfg.GetSiteData(),
258+ Site: cfg.SiteData(),
261259 PageTitle: headerTxt.Title,
262- URL: template.URL(cfg.FullBlogURL(curl, username)),
263- RSSURL: template.URL(cfg.RssBlogURL(curl, username, tag)),
260+ URL: template.URL(curl.FullBlogURL(username)),
261+ RSSURL: template.URL(curl.RssBlogURL(username, tag)),
264262 Readme: readmeTxt,
265263 Header: headerTxt,
266264 Username: username,
267265 Posts: postCollection,
268266 HasCSS: hasCSS,
269- CssURL: template.URL(cfg.CssURL(username)),
267+ CssURL: template.URL(cfg.CssURL()),
270268 HasFilter: tag != "",
271269 }
272270
......@@ -278,7 +276,7 @@ func (web *webRouter) blogHandler(w http.ResponseWriter, r *http.Request) {
278276 }
279277
280278 func (web *webRouter) postRawHandler(w http.ResponseWriter, r *http.Request) {
281- username := router.GetUsernameFromRequest(r, web.cfg)
279+ username := router.GetUsernameFromRequest(r)
282280 cfg := web.cfg
283281 slug, _ := url.PathUnescape(r.PathValue("slug"))
284282 slug = strings.TrimSuffix(slug, "/")
......@@ -312,7 +310,7 @@ func (web *webRouter) postRawHandler(w http.ResponseWriter, r *http.Request) {
312310 }
313311
314312 func (web *webRouter) robotsHandler(w http.ResponseWriter, r *http.Request) {
315- username := router.GetUsernameFromRequest(r, web.cfg)
313+ username := router.GetUsernameFromRequest(r)
316314 cfg := web.cfg
317315 dbpool := web.db
318316 logger := web.cfg.Logger
......@@ -338,7 +336,7 @@ func (web *webRouter) robotsHandler(w http.ResponseWriter, r *http.Request) {
338336 }
339337
340338 func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
341- username := router.GetUsernameFromRequest(r, web.cfg)
339+ username := router.GetUsernameFromRequest(r)
342340 cfg := web.cfg
343341 slug, _ := url.PathUnescape(r.PathValue("slug"))
344342 slug = strings.TrimSuffix(slug, "/")
......@@ -357,7 +355,7 @@ func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
357355 logger = logger.With("slug", slug)
358356
359357 blogName := GetBlogName(username)
360- curl := shared.CreateURLFromRequest(cfg, r)
358+ curl := shared.CreateURLFromRequest(cfg.Protocol, cfg.Domain, r)
361359
362360 favicon := ""
363361 ogImage := ""
......@@ -441,10 +439,10 @@ func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
441439 }
442440
443441 data = PostPageData{
444- Site: *cfg.GetSiteData(),
442+ Site: cfg.SiteData(),
445443 PageTitle: GetPostTitle(post),
446- URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
447- BlogURL: template.URL(cfg.FullBlogURL(curl, username)),
444+ URL: template.URL(curl.FullPostURL(post.Username, post.Slug)),
445+ BlogURL: template.URL(curl.FullBlogURL(username)),
448446 Description: post.Description,
449447 Title: shared.FilenameToTitle(post.Filename, post.Title),
450448 Slug: post.Slug,
......@@ -456,7 +454,7 @@ func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
456454 BlogName: blogName,
457455 Contents: contents,
458456 HasCSS: hasCSS,
459- CssURL: template.URL(cfg.CssURL(username)),
457+ CssURL: template.URL(cfg.CssURL()),
460458 Tags: tags,
461459 Image: template.URL(ogImage),
462460 ImageCard: ogImageCard,
......@@ -492,8 +490,8 @@ func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
492490 now := time.Now()
493491
494492 data = PostPageData{
495- Site: *cfg.GetSiteData(),
496- BlogURL: template.URL(cfg.FullBlogURL(curl, username)),
493+ Site: cfg.SiteData(),
494+ BlogURL: template.URL(curl.FullBlogURL(username)),
497495 PageTitle: title,
498496 Description: desc,
499497 Title: title,
......@@ -504,7 +502,7 @@ func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
504502 Username: username,
505503 BlogName: blogName,
506504 HasCSS: hasCSS,
507- CssURL: template.URL(cfg.CssURL(username)),
505+ CssURL: template.URL(cfg.CssURL()),
508506 Image: template.URL(ogImage),
509507 ImageCard: ogImageCard,
510508 Favicon: template.URL(favicon),
......@@ -515,7 +513,7 @@ func (web *webRouter) postHandler(w http.ResponseWriter, r *http.Request) {
515513 w.WriteHeader(http.StatusNotFound)
516514 }
517515
518- ts, err := router.RenderTemplate(cfg, []string{
516+ ts, err := router.RenderTemplate(staticDir, []string{
519517 cfg.StaticPath("html/list.partial.tmpl"),
520518 cfg.StaticPath("html/post.page.tmpl"),
521519 })
......@@ -545,7 +543,7 @@ func (web *webRouter) readHandler(w http.ResponseWriter, r *http.Request) {
545543 return
546544 }
547545
548- ts, err := router.RenderTemplate(cfg, []string{
546+ ts, err := router.RenderTemplate(staticDir, []string{
549547 cfg.StaticPath("html/read.page.tmpl"),
550548 })
551549
......@@ -554,14 +552,14 @@ func (web *webRouter) readHandler(w http.ResponseWriter, r *http.Request) {
554552 }
555553
556554 data := ReadPageData{
557- Site: *cfg.GetSiteData(),
555+ Site: cfg.SiteData(),
558556 }
559557
560- curl := shared.NewCreateURL(cfg)
558+ curl := shared.NewCreateURL(cfg.Protocol, cfg.Domain)
561559 for _, post := range posts {
562560 item := PostItemData{
563- URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
564- BlogURL: template.URL(cfg.FullBlogURL(curl, post.Username)),
561+ URL: template.URL(curl.FullPostURL(post.Username, post.Slug)),
562+ BlogURL: template.URL(curl.FullBlogURL(post.Username)),
565563 Title: shared.FilenameToTitle(post.Filename, post.Title),
566564 Description: post.Description,
567565 Username: post.Username,
......@@ -582,7 +580,7 @@ func (web *webRouter) readHandler(w http.ResponseWriter, r *http.Request) {
582580 }
583581
584582 func (web *webRouter) rssBlogHandler(w http.ResponseWriter, r *http.Request) {
585- username := router.GetUsernameFromRequest(r, web.cfg)
583+ username := router.GetUsernameFromRequest(r)
586584 dbpool := web.db
587585 logger := web.cfg.Logger
588586 cfg := web.cfg
......@@ -642,8 +640,8 @@ func (web *webRouter) rssBlogHandler(w http.ResponseWriter, r *http.Request) {
642640 }
643641 }
644642
645- curl := shared.CreateURLFromRequest(cfg, r)
646- blogUrl := cfg.FullBlogURL(curl, username)
643+ curl := shared.CreateURLFromRequest(cfg.Protocol, cfg.Domain, r)
644+ blogUrl := curl.FullBlogURL(username)
647645
648646 updatedAt := &time.Time{}
649647 if len(posts) > 0 {
......@@ -708,7 +706,7 @@ func (web *webRouter) rssBlogHandler(w http.ResponseWriter, r *http.Request) {
708706
709707 }
710708
711- realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
709+ realUrl := curl.FullPostURL(post.Username, post.Slug)
712710 feedId := realUrl
713711
714712 item := &feeds.Item{
......@@ -772,7 +770,7 @@ func (web *webRouter) rssHandler(w http.ResponseWriter, r *http.Request) {
772770 Created: time.Now(),
773771 }
774772
775- curl := shared.CreateURLFromRequest(cfg, r)
773+ curl := shared.CreateURLFromRequest(cfg.Protocol, cfg.Domain, r)
776774
777775 var feedItems []*feeds.Item
778776 for _, post := range posts {
......@@ -806,7 +804,7 @@ func (web *webRouter) rssHandler(w http.ResponseWriter, r *http.Request) {
806804
807805 }
808806
809- realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
807+ realUrl := curl.FullPostURL(post.Username, post.Slug)
810808 if !curl.Subdomain && !curl.UsernameInRoute {
811809 realUrl = fmt.Sprintf("%s://%s%s", cfg.Protocol, r.Host, realUrl)
812810 }
......@@ -847,7 +845,7 @@ func (web *webRouter) imgRequest(w http.ResponseWriter, r *http.Request) {
847845 logger := web.cfg.Logger
848846 st := web.storage
849847 dbpool := web.db
850- username := router.GetUsernameFromRequest(r, web.cfg)
848+ username := router.GetUsernameFromRequest(r)
851849 user, err := dbpool.FindUserByName(username)
852850 if err != nil {
853851 logger.Error("could not find user", "username", username)
......@@ -881,19 +879,20 @@ func (web *webRouter) imgRequest(w http.ResponseWriter, r *http.Request) {
881879 }
882880
883881 type webRouter struct {
884- cfg *shared.ConfigSite
882+ cfg *Config
885883 db DB
886884 storage storage.StorageServe
887885 }
888886
889887 func (web *webRouter) static(mux *http.ServeMux) {
890888 cfg := web.cfg
891- mux.HandleFunc("GET /read.css", router.ServeFile(cfg, "read.css", "text/css"))
892- mux.HandleFunc("GET /reset.css", router.ServeFile(cfg, "reset.css", "text/css"))
893- mux.HandleFunc("GET /syntax.css", router.ServeFile(cfg, "syntax.css", "text/css"))
894- mux.HandleFunc("GET /card.png", router.ServeFile(cfg, "card.png", "image/png"))
895- mux.HandleFunc("GET /favicon-16x16.png", router.ServeFile(cfg, "favicon-16x16.png", "image/png"))
896- mux.HandleFunc("GET /favicon.ico", router.ServeFile(cfg, "favicon.ico", "image/x-icon"))
889+ files := cfg.Static()
890+ mux.HandleFunc("GET /read.css", files.File("read.css", "text/css"))
891+ mux.HandleFunc("GET /reset.css", files.File("reset.css", "text/css"))
892+ mux.HandleFunc("GET /syntax.css", files.File("syntax.css", "text/css"))
893+ mux.HandleFunc("GET /card.png", files.File("card.png", "image/png"))
894+ mux.HandleFunc("GET /favicon-16x16.png", files.File("favicon-16x16.png", "image/png"))
895+ mux.HandleFunc("GET /favicon.ico", files.File("favicon.ico", "image/x-icon"))
897896 if cfg.Debug {
898897 router.CreatePProfRoutesMux(mux)
899898 }
......@@ -904,11 +903,11 @@ func (web *webRouter) mainMux() *http.ServeMux {
904903 mux := http.NewServeMux()
905904 mux.HandleFunc("GET /{$}", web.readHandler)
906905 mux.HandleFunc("GET /read", web.readHandler)
907- mux.HandleFunc("GET /check", router.CheckHandler(web.cfg, web.db))
906+ mux.HandleFunc("GET /check", router.CheckHandler(web.cfg.Domain, web.cfg.Space, web.db))
908907 mux.HandleFunc("GET /rss", web.rssHandler)
909908 mux.HandleFunc("GET /rss.atom", web.rssHandler)
910909 mux.Handle("GET /_metrics", promhttp.Handler())
911- mux.HandleFunc("GET /robots.txt", router.ServeFile(web.cfg, "robots.txt", "text/plain"))
910+ mux.HandleFunc("GET /robots.txt", web.cfg.Static().File("robots.txt", "text/plain"))
912911 web.static(mux)
913912 return mux
914913 }
......@@ -963,7 +962,7 @@ func (web *webRouter) pathRoute(w http.ResponseWriter, r *http.Request) {
963962 }
964963
965964 func StartApiServer() {
966- cfg := NewConfigSite("prose-web")
965+ cfg := NewConfig("prose-web")
967966 dbpool := NewDB(postgres.NewDB(cfg.DbURL, cfg.Logger))
968967 defer func() {
969968 _ = dbpool.Close()
......@@ -978,7 +977,7 @@ func StartApiServer() {
978977 }
979978
980979 web := &webRouter{cfg: cfg, db: dbpool, storage: st}
981- handler := router.NewHostRouter(cfg, web.mainMux(), web.sitesMux())
980+ handler := router.NewHostRouter(cfg.Domain, cfg.Space, web.mainMux(), web.sitesMux())
982981
983982 portStr := fmt.Sprintf(":%s", cfg.Port)
984983 logger.Info(
+2, -2
......@@ -20,7 +20,7 @@ func TestRoutes(t *testing.T) {
2020 logger := slog.New(slog.DiscardHandler)
2121 user := &db.User{ID: "user-1", Name: "tester"}
2222 posts := stub.NewPostsDB(logger, user)
23- cfg := NewConfigSite("prose-test")
23+ cfg := NewConfig("prose-test")
2424 cfg.Logger = logger
2525 post := filehandlers.NewPost("hello.md", []byte("# hello"))
2626 post.UserID, post.Space, post.Title = user.ID, cfg.Space, "Hello"
......@@ -32,7 +32,7 @@ func TestRoutes(t *testing.T) {
3232 t.Fatal(err)
3333 }
3434 web := &webRouter{cfg: cfg, db: posts, storage: st}
35- handler := router.NewHostRouter(cfg, web.mainMux(), web.sitesMux())
35+ handler := router.NewHostRouter(cfg.Domain, cfg.Space, web.mainMux(), web.sitesMux())
3636
3737 site := "https://tester." + cfg.Domain
3838 for _, tt := range []struct {
+48, -2
......@@ -1,14 +1,37 @@
11 package prose
22
33 import (
4+ "fmt"
5+ "html/template"
6+ "log/slog"
7+ "path/filepath"
48 "strings"
59
610 "github.com/picosh/pico/pkg/shared"
11+ "github.com/picosh/pico/pkg/shared/router"
712 )
813
914 var MAX_FILE_SIZE = 3 * shared.MB
1015
11-func NewConfigSite(service string) *shared.ConfigSite {
16+// staticDir holds prose's templates and public files.
17+const staticDir = "pkg/apps/prose"
18+
19+type Config struct {
20+ Debug bool
21+ Domain string
22+ Port string
23+ Protocol string
24+ DbURL string
25+ Space string
26+ AllowedExt []string
27+ HiddenPosts []string
28+ MaxSize uint64
29+ MaxAssetSize int64
30+ MaxSpecialFileSize int64
31+ Logger *slog.Logger
32+}
33+
34+func NewConfig(service string) *Config {
1235 debug := shared.GetEnv("PROSE_DEBUG", "0")
1336 domain := shared.GetEnv("PROSE_DOMAIN", "prose.sh")
1437 port := shared.GetEnv("PROSE_WEB_PORT", "3000")
......@@ -18,7 +41,7 @@ func NewConfigSite(service string) *shared.ConfigSite {
1841 maxImgSize := int64(10 * shared.MB)
1942 withPipe := strings.ToLower(shared.GetEnv("PICO_PIPE_ENABLED", "true")) == "true"
2043
21- return &shared.ConfigSite{
44+ return &Config{
2245 Debug: debug == "1",
2346 Domain: domain,
2447 Port: port,
......@@ -42,3 +65,26 @@ func NewConfigSite(service string) *shared.ConfigSite {
4265 MaxAssetSize: maxImgSize,
4366 }
4467 }
68+
69+func (c *Config) StaticPath(fname string) string {
70+ return filepath.Join(staticDir, fname)
71+}
72+
73+func (c *Config) SiteData() shared.SitePageData {
74+ return shared.SitePageData{
75+ Domain: template.URL(c.Domain),
76+ HomeURL: template.URL("//" + c.Domain),
77+ }
78+}
79+
80+func (c *Config) Static() router.Static {
81+ return router.Static{Dir: staticDir, Logger: c.Logger, Site: c.SiteData()}
82+}
83+
84+func (c *Config) ReadURL() string {
85+ return fmt.Sprintf("%s://%s", c.Protocol, c.Domain)
86+}
87+
88+func (c *Config) CssURL() string {
89+ return "/_styles.css"
90+}
R pkg/filehandlers/imgs/handler.go => pkg/apps/prose/imgs.go
+13, -13
......@@ -1,4 +1,4 @@
1-package uploadimgs
1+package prose
22
33 import (
44 "bytes"
......@@ -19,8 +19,6 @@ import (
1919 "github.com/picosh/pico/pkg/storage"
2020 )
2121
22-var Space = "imgs"
23-
2422 type PostMetaData struct {
2523 Text []byte
2624 FileSize int
......@@ -32,12 +30,12 @@ type PostMetaData struct {
3230 }
3331
3432 type UploadImgHandler struct {
35- DBPool shared.FeatureFinder
36- Cfg *shared.ConfigSite
33+ DBPool DB
34+ Cfg *Config
3735 Storage storage.StorageServe
3836 }
3937
40-func NewUploadImgHandler(dbpool shared.FeatureFinder, cfg *shared.ConfigSite, storage storage.StorageServe) *UploadImgHandler {
38+func NewUploadImgHandler(dbpool DB, cfg *Config, storage storage.StorageServe) *UploadImgHandler {
4139 return &UploadImgHandler{
4240 DBPool: dbpool,
4341 Cfg: cfg,
......@@ -171,7 +169,13 @@ func (h *UploadImgHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.
171169 }
172170
173171 fileSize := binary.Size(text)
174- featureFlag := shared.FindPlusFF(h.DBPool, h.Cfg, user.ID)
172+ featureFlag, _ := h.DBPool.FindFeature(user.ID, "plus")
173+ // we have free tiers so users might not have a feature flag
174+ // in which case we set sane defaults
175+ if featureFlag == nil {
176+ featureFlag = db.NewFeatureFlag(user.ID, "plus", h.Cfg.MaxSize, h.Cfg.MaxAssetSize, h.Cfg.MaxSpecialFileSize)
177+ }
178+ featureFlag.SetLimits(h.Cfg.MaxSize, h.Cfg.MaxAssetSize, h.Cfg.MaxSpecialFileSize)
175179
176180 bucket, err := h.Storage.UpsertBucket(shared.GetAssetBucketName(user.ID))
177181 if err != nil {
......@@ -200,12 +204,8 @@ func (h *UploadImgHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.
200204 return "", err
201205 }
202206
203- curl := shared.NewCreateURL(h.Cfg)
204- url := h.Cfg.FullPostURL(
205- curl,
206- user.Name,
207- metadata.Filename,
208- )
207+ curl := shared.NewCreateURL(h.Cfg.Protocol, h.Cfg.Domain)
208+ url := curl.FullPostURL(user.Name, metadata.Filename)
209209 return url + shared.StorageUsage(metadata.TotalFileSize+fileSize, int(featureFlag.Data.StorageMax)), nil
210210 }
211211
+2, -2
......@@ -16,7 +16,7 @@ import (
1616
1717 // postUploader stores prose's text files as posts.
1818 type postUploader struct {
19- Cfg *shared.ConfigSite
19+ Cfg *Config
2020 DB DB
2121 }
2222
......@@ -98,7 +98,7 @@ func (h *postUploader) Write(s *pssh.SSHServerConnSession, entry *sendutils.File
9898 }
9999 }
100100
101- return h.Cfg.FullPostURL(shared.NewCreateURL(h.Cfg), user.Name, post.Slug), nil
101+ return shared.NewCreateURL(h.Cfg.Protocol, h.Cfg.Domain).FullPostURL(user.Name, post.Slug), nil
102102 }
103103
104104 // Rename moves a post to a new filename in place, so it keeps its ID,
+1, -1
......@@ -25,7 +25,7 @@ func StartSshServer() {
2525 host := shared.GetEnv("PROSE_HOST", "0.0.0.0")
2626 port := shared.GetEnv("PROSE_SSH_PORT", "2222")
2727 promPort := shared.GetEnv("PROSE_PROM_PORT", "9222")
28- cfg := NewConfigSite(appName)
28+ cfg := NewConfig(appName)
2929 logger := cfg.Logger
3030
3131 ctx, cancel := context.WithCancel(context.Background())
+7, -43
......@@ -3,14 +3,11 @@ package prose
33 import (
44 "fmt"
55 "log/slog"
6- "strings"
76
87 "github.com/picosh/pico/pkg/db"
98 "github.com/picosh/pico/pkg/filehandlers"
10- uploadimgs "github.com/picosh/pico/pkg/filehandlers/imgs"
119 "github.com/picosh/pico/pkg/pssh"
1210 sendutils "github.com/picosh/pico/pkg/send/utils"
13- "github.com/picosh/pico/pkg/shared"
1411 "github.com/picosh/pico/pkg/storage"
1512 )
1613
......@@ -29,51 +26,17 @@ func setFeatureFlag(s *pssh.SSHServerConnSession, ff *db.FeatureFlag) {
2926 s.SetValue(ctxFeatureFlagKey{}, ff)
3027 }
3128
32-func setFeatureLimits(ff *db.FeatureFlag, cfg *shared.ConfigSite) {
33- ff.Data.StorageMax = ff.FindStorageMax(cfg.MaxSize)
34- ff.Data.FileMax = ff.FindFileMax(cfg.MaxAssetSize)
35- ff.Data.SpecialFileMax = ff.FindSpecialFileMax(cfg.MaxSpecialFileSize)
36-}
37-
38-func findFeatureFlag(dbpool DB, cfg *shared.ConfigSite, userID string) (*db.FeatureFlag, error) {
39- ff, err := dbpool.FindFeature(userID, "plus")
40- if err == nil {
41- if ff.IsValid() {
42- setFeatureLimits(ff, cfg)
43- return ff, nil
44- }
45- err = fmt.Errorf("ERROR: your pico+ has expired: https://blog.pico.sh/ann-038-pico-invite-system")
46- }
47-
48- ffProse, proseErr := dbpool.FindFeature(userID, "prose")
49- if proseErr == nil {
50- if ffProse.IsValid() {
51- setFeatureLimits(ffProse, cfg)
52- return ffProse, nil
53- }
54- proseErr = fmt.Errorf("ERROR: your prose access has expired: https://blog.pico.sh/ann-038-pico-invite-system")
55- }
56-
57- if err != nil && strings.Contains(err.Error(), "expired") {
58- return nil, err
59- }
60- if proseErr != nil && strings.Contains(proseErr.Error(), "expired") {
61- return nil, proseErr
62- }
63- return nil, fmt.Errorf("ERROR: uploading to prose requires an invitation or pico+: https://blog.pico.sh/ann-038-pico-invite-system")
64-}
65-
6629 type UploadHandler struct {
6730 *filehandlers.FileHandlerRouter
68- Cfg *shared.ConfigSite
31+ Cfg *Config
6932 DB DB
7033 }
7134
7235 var _ sendutils.CopyFromClientHandler = &UploadHandler{}
7336 var _ sendutils.CopyFromClientHandler = (*UploadHandler)(nil)
7437
75-func NewUploadHandler(cfg *shared.ConfigSite, dbpool DB, fileMap map[string]filehandlers.ReadWriteHandler) *UploadHandler {
76- router := filehandlers.NewFileHandlerRouter(cfg, fileMap)
38+func NewUploadHandler(cfg *Config, dbpool DB, fileMap map[string]filehandlers.ReadWriteHandler) *UploadHandler {
39+ router := filehandlers.NewFileHandlerRouter(cfg.Space, fileMap)
7740 return &UploadHandler{
7841 FileHandlerRouter: router,
7942 Cfg: cfg,
......@@ -82,7 +45,7 @@ func NewUploadHandler(cfg *shared.ConfigSite, dbpool DB, fileMap map[string]file
8245 }
8346
8447 // NewFileHandler routes prose's text files to posts and the rest to images.
85-func NewFileHandler(cfg *shared.ConfigSite, dbpool DB, st storage.StorageServe) *UploadHandler {
48+func NewFileHandler(cfg *Config, dbpool DB, st storage.StorageServe) *UploadHandler {
8649 // One post handler serves every text extension, so a post can be
8750 // renamed in place from one to another.
8851 posts := &postUploader{Cfg: cfg, DB: dbpool}
......@@ -91,7 +54,7 @@ func NewFileHandler(cfg *shared.ConfigSite, dbpool DB, st storage.StorageServe)
9154 ".txt": posts,
9255 ".css": posts,
9356 ".lxt": posts,
94- "fallback": uploadimgs.NewUploadImgHandler(dbpool, cfg, st),
57+ "fallback": NewUploadImgHandler(dbpool, cfg, st),
9558 })
9659 }
9760
......@@ -116,10 +79,11 @@ func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
11679 return err
11780 }
11881
119- ff, err := findFeatureFlag(h.DB, h.Cfg, user.ID)
82+ ff, err := db.FindAccess(h.DB, user.ID, "prose")
12083 if err != nil {
12184 return err
12285 }
86+ ff.SetLimits(h.Cfg.MaxSize, h.Cfg.MaxAssetSize, h.Cfg.MaxSpecialFileSize)
12387 setFeatureFlag(s, ff)
12488
12589 return h.FileHandlerRouter.Validate(s)
+2, -2
......@@ -44,7 +44,7 @@ func (m *mockFeatureDB) setFeature(userID string, ff *db.FeatureFlag) {
4444 }
4545
4646 func TestUploadHandlerValidate(t *testing.T) {
47- cfg := NewConfigSite("prose-test")
47+ cfg := NewConfig("prose-test")
4848 validExpires := time.Now().Add(24 * time.Hour)
4949 userID := "user-1"
5050
......@@ -124,7 +124,7 @@ func TestPostWriteTagsAndAliases(t *testing.T) {
124124 user := &db.User{ID: "user-1", Name: "tester"}
125125 pssh.SetUser(session, user)
126126 posts := stub.NewPostsDB(logger, user)
127- cfg := NewConfigSite("prose-test")
127+ cfg := NewConfig("prose-test")
128128 h := &postUploader{Cfg: cfg, DB: posts}
129129 write := func(text string, mtime int64) *db.Post {
130130 t.Helper()
+39, -0
......@@ -7,6 +7,7 @@ import (
77 "errors"
88 "fmt"
99 "regexp"
10+ "strings"
1011 "time"
1112 )
1213
......@@ -686,6 +687,44 @@ type DB interface {
686687 Close() error
687688 }
688689
690+// SetLimits fills in the flag's storage, file and special file limits, using
691+// the defaults where the flag sets none.
692+func (ff *FeatureFlag) SetLimits(storageMax uint64, fileMax, specialFileMax int64) {
693+ ff.Data.StorageMax = ff.FindStorageMax(storageMax)
694+ ff.Data.FileMax = ff.FindFileMax(fileMax)
695+ ff.Data.SpecialFileMax = ff.FindSpecialFileMax(specialFileMax)
696+}
697+
698+// FindAccess returns the flag that lets a user use a service: pico+, or the
699+// service's own feature. The error says why a user has neither.
700+func FindAccess(dbpool interface {
701+ FindFeature(userID string, feature string) (*FeatureFlag, error)
702+}, userID, service string) (*FeatureFlag, error) {
703+ ff, err := dbpool.FindFeature(userID, "plus")
704+ if err == nil {
705+ if ff.IsValid() {
706+ return ff, nil
707+ }
708+ err = fmt.Errorf("ERROR: your pico+ has expired: https://blog.pico.sh/ann-038-pico-invite-system")
709+ }
710+
711+ ffService, serviceErr := dbpool.FindFeature(userID, service)
712+ if serviceErr == nil {
713+ if ffService.IsValid() {
714+ return ffService, nil
715+ }
716+ serviceErr = fmt.Errorf("ERROR: your %s access has expired: https://blog.pico.sh/ann-038-pico-invite-system", service)
717+ }
718+
719+ if err != nil && strings.Contains(err.Error(), "expired") {
720+ return nil, err
721+ }
722+ if serviceErr != nil && strings.Contains(serviceErr.Error(), "expired") {
723+ return nil, serviceErr
724+ }
725+ return nil, fmt.Errorf("ERROR: uploading to %s requires an invitation or pico+: https://blog.pico.sh/ann-038-pico-invite-system", service)
726+}
727+
689728 func HasPlusOrSpace(dbpool interface {
690729 HasFeatureByUser(userID string, feature string) bool
691730 }, userID, space string) bool {
+1, -2
......@@ -15,7 +15,6 @@ import (
1515 "github.com/picosh/pico/pkg/db/stub"
1616 "github.com/picosh/pico/pkg/pssh"
1717 sendutils "github.com/picosh/pico/pkg/send/utils"
18- "github.com/picosh/pico/pkg/shared"
1918 "golang.org/x/crypto/ssh"
2019 )
2120
......@@ -140,7 +139,7 @@ func (otherHandler) List(*pssh.SSHServerConnSession, string, bool, bool) ([]os.F
140139 func TestRouterSharedHandler(t *testing.T) {
141140 session, posts := newPostsSession(t)
142141 postHandler := &postsHandler{db: posts}
143- router := NewFileHandlerRouter(&shared.ConfigSite{Space: "prose"}, map[string]ReadWriteHandler{
142+ router := NewFileHandlerRouter("prose", map[string]ReadWriteHandler{
144143 ".md": postHandler,
145144 ".lxt": postHandler,
146145 ".txt": &postsHandler{db: posts},
+4, -5
......@@ -11,7 +11,6 @@ import (
1111 "github.com/picosh/pico/pkg/db"
1212 "github.com/picosh/pico/pkg/pssh"
1313 "github.com/picosh/pico/pkg/send/utils"
14- "github.com/picosh/pico/pkg/shared"
1514 )
1615
1716 type ReadWriteHandler interface {
......@@ -23,15 +22,15 @@ type ReadWriteHandler interface {
2322
2423 type FileHandlerRouter struct {
2524 FileMap map[string]ReadWriteHandler
26- Cfg *shared.ConfigSite
25+ Space string
2726 }
2827
2928 var _ utils.CopyFromClientHandler = &FileHandlerRouter{} // Verify implementation
3029 var _ utils.CopyFromClientHandler = (*FileHandlerRouter)(nil) // Verify implementation
3130
32-func NewFileHandlerRouter(cfg *shared.ConfigSite, mapper map[string]ReadWriteHandler) *FileHandlerRouter {
31+func NewFileHandlerRouter(space string, mapper map[string]ReadWriteHandler) *FileHandlerRouter {
3332 return &FileHandlerRouter{
34- Cfg: cfg,
33+ Space: space,
3534 FileMap: mapper,
3635 }
3736 }
......@@ -150,7 +149,7 @@ func (r *FileHandlerRouter) Validate(s *pssh.SSHServerConnSession) error {
150149 logger.Info(
151150 "attempting to upload files",
152151 "user", user.Name,
153- "space", r.Cfg.Space,
152+ "space", r.Space,
154153 )
155154 return nil
156155 }
+8, -5
......@@ -50,24 +50,27 @@ func newPostsEnv(t *testing.T, app string) *postsEnv {
5050 posts := stub.NewPostsDB(logger, &db.User{ID: "user-1", Name: "tester"})
5151 posts.Pubkey = shared.KeyForKeyText(signer.PublicKey())
5252
53- var cfg *shared.ConfigSite
53+ var space string
5454 var handler interface {
5555 utils.CopyFromClientHandler
5656 pssh.GetLoggerInterface
5757 }
5858 switch app {
5959 case "prose":
60- cfg = prose.NewConfigSite("prose-test")
60+ cfg := prose.NewConfig("prose-test")
61+ cfg.Logger = logger
6162 st, err := storage.NewStorageMemory(map[string]map[string]string{})
6263 if err != nil {
6364 t.Fatal(err)
6465 }
6566 handler = prose.NewFileHandler(cfg, posts, st)
67+ space = cfg.Space
6668 case "pastes":
67- cfg = pastes.NewConfigSite("pastes-test")
69+ cfg := pastes.NewConfig("pastes-test")
70+ cfg.Logger = logger
6871 handler = pastes.NewUploadHandler(cfg, posts)
72+ space = cfg.Space
6973 }
70- cfg.Logger = logger
7174
7275 ctx, cancel := context.WithCancel(context.Background())
7376 t.Cleanup(cancel)
......@@ -107,7 +110,7 @@ func newPostsEnv(t *testing.T, app string) *postsEnv {
107110 if err := os.WriteFile(key, pem.EncodeToMemory(b), 0o600); err != nil {
108111 t.Fatal(err)
109112 }
110- return &postsEnv{t: t, db: posts, space: cfg.Space, port: port, key: key}
113+ return &postsEnv{t: t, db: posts, space: space, port: port, key: key}
111114 }
112115
113116 func (e *postsEnv) sshOpts() []string {
+26, -189
......@@ -8,7 +8,6 @@ import (
88 "net/http"
99 "net/url"
1010 "os"
11- "path"
1211 "strings"
1312 "time"
1413
......@@ -28,169 +27,72 @@ type PageData struct {
2827 Site SitePageData
2928 }
3029
31-type ConfigSite struct {
32- Debug bool
33- SendgridKey string
34- Domain string
35- Port string
36- PortOverride string
37- Protocol string
38- DbURL string
39- Space string
40- Issuer string
41- Secret string
42- SecretWebhook string
43- AllowedExt []string
44- HiddenPosts []string
45- MaxSize uint64
46- MaxAssetSize int64
47- MaxSpecialFileSize int64
48- Logger *slog.Logger
49- TunsSecret string
50-}
51-
52-func NewConfigSite() *ConfigSite {
53- return &ConfigSite{}
54-}
55-
30+// CreateURL builds links to users' sites on Domain, as seen from the host a
31+// request came in on.
5632 type CreateURL struct {
33+ Protocol string
34+ Domain string
5735 Subdomain bool
5836 UsernameInRoute bool
5937 HostDomain string
60- AppDomain string
61- Username string
62- Cfg *ConfigSite
6338 }
6439
65-func NewCreateURL(cfg *ConfigSite) *CreateURL {
40+// NewCreateURL builds links that point at users' subdomains.
41+func NewCreateURL(protocol, domain string) *CreateURL {
6642 return &CreateURL{
67- Cfg: cfg,
68- Subdomain: cfg.IsSubdomains(),
43+ Protocol: protocol,
44+ Domain: domain,
45+ Subdomain: true,
6946 }
7047 }
7148
72-func CreateURLFromRequest(cfg *ConfigSite, r *http.Request) *CreateURL {
49+func CreateURLFromRequest(protocol, domain string, r *http.Request) *CreateURL {
7350 hostDomain := strings.Split(r.Host, ":")[0]
74- appDomain := strings.Split(cfg.Domain, ":")[0]
51+ appDomain := strings.Split(domain, ":")[0]
7552
76- onSubdomain := cfg.IsSubdomains() && strings.Contains(hostDomain, appDomain)
77- withUserName := !cfg.IsCustomdomains() || (!onSubdomain && hostDomain == appDomain)
53+ onSubdomain := strings.Contains(hostDomain, appDomain)
54+ withUserName := !onSubdomain && hostDomain == appDomain
7855
7956 return &CreateURL{
80- Cfg: cfg,
81- AppDomain: appDomain,
57+ Protocol: protocol,
58+ Domain: domain,
8259 HostDomain: hostDomain,
8360 Subdomain: onSubdomain,
8461 UsernameInRoute: withUserName,
8562 }
8663 }
8764
88-func (c *ConfigSite) GetSiteData() *SitePageData {
89- return &SitePageData{
90- Domain: template.URL(c.Domain),
91- HomeURL: template.URL(c.HomeURL()),
92- }
93-}
94-
95-func (c *ConfigSite) IsSubdomains() bool {
96- return true
97-}
98-
99-func (c *ConfigSite) IsCustomdomains() bool {
100- return true
101-}
102-
103-func (c *ConfigSite) HomeURL() string {
104- if c.IsSubdomains() || c.IsCustomdomains() {
105- return fmt.Sprintf("//%s", c.Domain)
106- }
107-
108- return "/"
109-}
110-
111-func (c *ConfigSite) ReadURL() string {
112- if c.IsSubdomains() || c.IsCustomdomains() {
113- return fmt.Sprintf("%s://%s", c.Protocol, c.Domain)
114- }
115-
116- return "/"
117-}
118-
119-func (c *ConfigSite) StaticPath(fname string) string {
120- return path.Join("pkg", "apps", c.Space, fname)
121-}
122-
123-func (c *ConfigSite) BlogURL(username string) string {
124- if c.IsSubdomains() {
125- return fmt.Sprintf("%s://%s.%s", c.Protocol, username, c.Domain)
126- }
127-
128- return fmt.Sprintf("/%s", username)
129-}
130-
131-func (c *ConfigSite) CssURL(username string) string {
132- if c.IsSubdomains() || c.IsCustomdomains() {
133- return "/_styles.css"
134- }
135-
136- return fmt.Sprintf("/%s/styles.css", username)
137-}
138-
139-func (c *ConfigSite) PostURL(username, slug string) string {
140- fname := url.PathEscape(slug)
141- if c.IsSubdomains() {
142- return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
143- }
144-
145- return fmt.Sprintf("/%s/%s", username, fname)
146-
147-}
148-
149-func (c *ConfigSite) RawPostURL(username, slug string) string {
150- fname := url.PathEscape(slug)
151- if c.IsSubdomains() {
152- return fmt.Sprintf("%s://%s.%s/raw/%s", c.Protocol, username, c.Domain, fname)
153- }
154-
155- return fmt.Sprintf("/raw/%s/%s", username, fname)
156-}
157-
158-func (c *ConfigSite) ImgFullURL(username, slug string) string {
159- fname := url.PathEscape(strings.TrimLeft(slug, "/"))
160- return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
161-}
162-
163-func (c *ConfigSite) FullBlogURL(curl *CreateURL, username string) string {
164- if c.IsSubdomains() && curl.Subdomain {
65+func (c *CreateURL) FullBlogURL(username string) string {
66+ if c.Subdomain {
16567 return fmt.Sprintf("%s://%s.%s", c.Protocol, username, c.Domain)
16668 }
16769
168- if curl.UsernameInRoute {
70+ if c.UsernameInRoute {
16971 return fmt.Sprintf("/%s", username)
17072 }
17173
172- return fmt.Sprintf("%s://%s", c.Protocol, curl.HostDomain)
74+ return fmt.Sprintf("%s://%s", c.Protocol, c.HostDomain)
17375 }
17476
175-func (c *ConfigSite) FullPostURL(curl *CreateURL, username, slug string) string {
77+func (c *CreateURL) FullPostURL(username, slug string) string {
17678 fname := url.PathEscape(strings.TrimLeft(slug, "/"))
17779
178- if curl.Subdomain && c.IsSubdomains() {
80+ if c.Subdomain {
17981 return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
18082 }
18183
182- if curl.UsernameInRoute {
84+ if c.UsernameInRoute {
18385 return fmt.Sprintf("%s://%s/%s/%s", c.Protocol, c.Domain, username, fname)
18486 }
18587
186- return fmt.Sprintf("%s://%s/%s", c.Protocol, curl.HostDomain, fname)
88+ return fmt.Sprintf("%s://%s/%s", c.Protocol, c.HostDomain, fname)
18789 }
18890
189-func (c *ConfigSite) RssBlogURL(curl *CreateURL, username, tag string) string {
91+func (c *CreateURL) RssBlogURL(username, tag string) string {
19092 url := ""
191- if c.IsSubdomains() && curl.Subdomain {
93+ if c.Subdomain {
19294 url = fmt.Sprintf("%s://%s.%s/rss", c.Protocol, username, c.Domain)
193- } else if curl.UsernameInRoute {
95+ } else if c.UsernameInRoute {
19496 url = fmt.Sprintf("/%s/rss", username)
19597 } else {
19698 url = "/rss"
......@@ -203,71 +105,6 @@ func (c *ConfigSite) RssBlogURL(curl *CreateURL, username, tag string) string {
203105 return url
204106 }
205107
206-func (c *ConfigSite) ImgURL(curl *CreateURL, username string, slug string) string {
207- fname := url.PathEscape(strings.TrimLeft(slug, "/"))
208- if c.IsSubdomains() && curl.Subdomain {
209- return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
210- }
211-
212- if curl.UsernameInRoute {
213- return fmt.Sprintf("/%s/%s", username, fname)
214- }
215-
216- return fmt.Sprintf("/%s", fname)
217-}
218-
219-func (c *ConfigSite) ImgPostURL(curl *CreateURL, username string, slug string) string {
220- fname := url.PathEscape(strings.TrimLeft(slug, "/"))
221- if c.IsSubdomains() && curl.Subdomain {
222- return fmt.Sprintf("%s://%s.%s/p/%s", c.Protocol, username, c.Domain, fname)
223- }
224-
225- if curl.UsernameInRoute {
226- return fmt.Sprintf("/%s/p/%s", username, fname)
227- }
228-
229- return fmt.Sprintf("/p/%s", fname)
230-}
231-
232-func (c *ConfigSite) ImgOrigURL(curl *CreateURL, username string, slug string) string {
233- fname := url.PathEscape(strings.TrimLeft(slug, "/"))
234- if c.IsSubdomains() && curl.Subdomain {
235- return fmt.Sprintf("%s://%s.%s/o/%s", c.Protocol, username, c.Domain, fname)
236- }
237-
238- if curl.UsernameInRoute {
239- return fmt.Sprintf("/%s/o/%s", username, fname)
240- }
241-
242- return fmt.Sprintf("/o/%s", fname)
243-}
244-
245-func (c *ConfigSite) TagURL(curl *CreateURL, username, tag string) string {
246- tg := url.PathEscape(tag)
247- return fmt.Sprintf("%s?tag=%s", c.FullBlogURL(curl, username), tg)
248-}
249-
250-func (c *ConfigSite) AssetURL(username, projectName, fpath string) string {
251- if username == projectName {
252- return fmt.Sprintf(
253- "%s://%s.%s/%s",
254- c.Protocol,
255- username,
256- c.Domain,
257- fpath,
258- )
259- }
260-
261- return fmt.Sprintf(
262- "%s://%s-%s.%s/%s",
263- c.Protocol,
264- username,
265- projectName,
266- c.Domain,
267- fpath,
268- )
269-}
270-
271108 func CreateLogger(space string, withPipe bool) *slog.Logger {
272109 logger := slog.New(
273110 slog.NewTextHandler(
+56, -52
......@@ -1,10 +1,11 @@
11 package router
22
33 import (
4- "fmt"
54 "html/template"
5+ "log/slog"
66 "net/http"
77 "os"
8+ "path/filepath"
89 "strings"
910
1011 "github.com/picosh/pico/pkg/db"
......@@ -41,23 +42,22 @@ func UnauthorizedHandler(w http.ResponseWriter, r *http.Request) {
4142 http.Error(w, "You do not have access to this site", http.StatusUnauthorized)
4243 }
4344
44-// CheckHandler reports whether a custom domain points at a user's site.
45-func CheckHandler(cfg *shared.ConfigSite, users interface {
45+// CheckHandler reports whether a custom domain points at a user's site on
46+// domain, through its TXT record for txtPrefix.
47+func CheckHandler(domain, txtPrefix string, users interface {
4648 FindUserByName(name string) (*db.User, error)
4749 }) http.HandlerFunc {
4850 return func(w http.ResponseWriter, r *http.Request) {
49- if cfg.IsCustomdomains() {
50- hostDomain := r.URL.Query().Get("domain")
51- appDomain := strings.Split(cfg.Domain, ":")[0]
52-
53- if !strings.Contains(hostDomain, appDomain) {
54- subdomain := GetCustomDomain(hostDomain, cfg.Space)
55- if subdomain != "" {
56- u, err := users.FindUserByName(subdomain)
57- if u != nil && err == nil {
58- w.WriteHeader(http.StatusOK)
59- return
60- }
51+ hostDomain := r.URL.Query().Get("domain")
52+ appDomain := strings.Split(domain, ":")[0]
53+
54+ if !strings.Contains(hostDomain, appDomain) {
55+ subdomain := GetCustomDomain(hostDomain, txtPrefix)
56+ if subdomain != "" {
57+ u, err := users.FindUserByName(subdomain)
58+ if u != nil && err == nil {
59+ w.WriteHeader(http.StatusOK)
60+ return
6161 }
6262 }
6363 }
......@@ -68,31 +68,11 @@ func CheckHandler(cfg *shared.ConfigSite, users interface {
6868
6969 // GetUsernameFromRequest is the user whose site a request is for: the
7070 // subdomain, or the path's {user} on the main domain.
71-func GetUsernameFromRequest(r *http.Request, cfg *shared.ConfigSite) string {
72- subdomain := GetSubdomain(r)
73- if !cfg.IsSubdomains() || subdomain == "" {
74- return r.PathValue("user")
75- }
76- return subdomain
77-}
78-
79-func ServeFile(cfg *shared.ConfigSite, file string, contentType string) http.HandlerFunc {
80- return func(w http.ResponseWriter, r *http.Request) {
81- logger := cfg.Logger
82-
83- contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
84- if err != nil {
85- logger.Error(err.Error())
86- http.Error(w, "file not found", 404)
87- }
88-
89- w.Header().Add("Content-Type", contentType)
90-
91- _, err = w.Write(contents)
92- if err != nil {
93- logger.Error(err.Error())
94- }
71+func GetUsernameFromRequest(r *http.Request) string {
72+ if subdomain := GetSubdomain(r); subdomain != "" {
73+ return subdomain
9574 }
75+ return r.PathValue("user")
9676 }
9777
9878 func minus(a, b int) int {
......@@ -113,14 +93,16 @@ var FuncMap = template.FuncMap{
11393 "intRange": intRange,
11494 }
11595
116-func RenderTemplate(cfg *shared.ConfigSite, templates []string) (*template.Template, error) {
96+// RenderTemplate parses templates with the layout and footers in an app's
97+// dir.
98+func RenderTemplate(dir string, templates []string) (*template.Template, error) {
11799 files := make([]string, len(templates))
118100 copy(files, templates)
119101 files = append(
120102 files,
121- cfg.StaticPath("html/footer.partial.tmpl"),
122- cfg.StaticPath("html/marketing-footer.partial.tmpl"),
123- cfg.StaticPath("html/base.layout.tmpl"),
103+ filepath.Join(dir, "html/footer.partial.tmpl"),
104+ filepath.Join(dir, "html/marketing-footer.partial.tmpl"),
105+ filepath.Join(dir, "html/base.layout.tmpl"),
124106 )
125107
126108 ts, err := template.New("base").Funcs(FuncMap).ParseFiles(files...)
......@@ -130,23 +112,45 @@ func RenderTemplate(cfg *shared.ConfigSite, templates []string) (*template.Templ
130112 return ts, nil
131113 }
132114
133-func CreatePageHandler(cfg *shared.ConfigSite, fname string) http.HandlerFunc {
115+// Static serves an app's files from Dir/public and its pages from Dir/html.
116+type Static struct {
117+ Dir string
118+ Logger *slog.Logger
119+ Site shared.SitePageData
120+}
121+
122+// File serves the file name in Dir/public.
123+func (s Static) File(name string, contentType string) http.HandlerFunc {
134124 return func(w http.ResponseWriter, r *http.Request) {
135- logger := cfg.Logger
136- ts, err := RenderTemplate(cfg, []string{cfg.StaticPath(fname)})
125+ contents, err := os.ReadFile(filepath.Join(s.Dir, "public", name))
126+ if err != nil {
127+ s.Logger.Error("could not read file", "fname", name, "err", err.Error())
128+ http.Error(w, "file not found", http.StatusNotFound)
129+ return
130+ }
131+
132+ w.Header().Add("Content-Type", contentType)
133+
134+ _, err = w.Write(contents)
135+ if err != nil {
136+ s.Logger.Error("could not write http response", "file", name, "err", err.Error())
137+ }
138+ }
139+}
137140
141+// Page renders the template fname in Dir with the site's data.
142+func (s Static) Page(fname string) http.HandlerFunc {
143+ return func(w http.ResponseWriter, r *http.Request) {
144+ ts, err := RenderTemplate(s.Dir, []string{filepath.Join(s.Dir, fname)})
138145 if err != nil {
139- logger.Error(err.Error())
146+ s.Logger.Error("could not render template", "fname", fname, "err", err.Error())
140147 http.Error(w, err.Error(), http.StatusInternalServerError)
141148 return
142149 }
143150
144- data := shared.PageData{
145- Site: *cfg.GetSiteData(),
146- }
147- err = ts.Execute(w, data)
151+ err = ts.Execute(w, shared.PageData{Site: s.Site})
148152 if err != nil {
149- logger.Error(err.Error())
153+ s.Logger.Error("could not execute template", "fname", fname, "err", err.Error())
150154 http.Error(w, err.Error(), http.StatusInternalServerError)
151155 }
152156 }
+14, -9
......@@ -56,30 +56,35 @@ func GetSubdomainFromRequest(r *http.Request, domain, space string) string {
5656 return ""
5757 }
5858
59-// NewHostRouter serves requests for users' sites, on subdomains and custom
60-// domains, with sites, and the rest with main. GetSubdomain returns the
61-// site's name. A nil sites serves everything with main.
62-func NewHostRouter(cfg *shared.ConfigSite, main, sites http.Handler) http.Handler {
59+// NewHostRouter serves requests for users' sites with sites, and the rest
60+// with main. A site is a subdomain of domain, or a custom domain whose TXT
61+// record for txtPrefix names one. GetSubdomain returns the site's name. A
62+// nil sites serves everything with main.
63+func NewHostRouter(domain, txtPrefix string, main, sites http.Handler) http.Handler {
6364 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6465 if sites == nil {
6566 main.ServeHTTP(w, r)
6667 return
6768 }
68- subdomain := GetSubdomainFromRequest(r, cfg.Domain, cfg.Space)
69+ subdomain := GetSubdomainFromRequest(r, domain, txtPrefix)
6970 if subdomain == "" {
7071 main.ServeHTTP(w, r)
7172 return
7273 }
73- ctx := context.WithValue(r.Context(), CtxSubdomainKey{}, subdomain)
74- sites.ServeHTTP(w, r.WithContext(ctx))
74+ sites.ServeHTTP(w, r.WithContext(WithSubdomain(r.Context(), subdomain)))
7575 })
7676 }
7777
78-type CtxSubdomainKey struct{}
78+type ctxSubdomainKey struct{}
79+
80+// WithSubdomain marks ctx as a request for the site subdomain.
81+func WithSubdomain(ctx context.Context, subdomain string) context.Context {
82+ return context.WithValue(ctx, ctxSubdomainKey{}, subdomain)
83+}
7984
8085 // GetSubdomain is the site a request is for, or "" for the main domain.
8186 func GetSubdomain(r *http.Request) string {
82- subdomain, _ := r.Context().Value(CtxSubdomainKey{}).(string)
87+ subdomain, _ := r.Context().Value(ctxSubdomainKey{}).(string)
8388 return subdomain
8489 }
8590
+2, -5
......@@ -6,8 +6,6 @@ import (
66 "net/http"
77 "net/http/httptest"
88 "testing"
9-
10- "github.com/picosh/pico/pkg/shared"
119 )
1210
1311 func TestGetCustomDomainCachesMisses(t *testing.T) {
......@@ -56,7 +54,6 @@ func TestGetCustomDomainCachesMisses(t *testing.T) {
5654 }
5755
5856 func TestNewHostRouter(t *testing.T) {
59- cfg := &shared.ConfigSite{Domain: "pico.test", Space: "prose"}
6057 serve := func(name string) http.Handler {
6158 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6259 _, _ = w.Write([]byte(name + ":" + GetSubdomain(r)))
......@@ -68,14 +65,14 @@ func TestNewHostRouter(t *testing.T) {
6865 return res.Body.String()
6966 }
7067
71- h := NewHostRouter(cfg, serve("main"), serve("sites"))
68+ h := NewHostRouter("pico.test", "prose", serve("main"), serve("sites"))
7269 if got := get(h, "https://pico.test/"); got != "main:" {
7370 t.Errorf("main domain = %q", got)
7471 }
7572 if got := get(h, "https://tester.pico.test/"); got != "sites:tester" {
7673 t.Errorf("subdomain = %q", got)
7774 }
78- if got := get(NewHostRouter(cfg, serve("main"), nil), "https://tester.pico.test/"); got != "main:" {
75+ if got := get(NewHostRouter("pico.test", "prose", serve("main"), nil), "https://tester.pico.test/"); got != "main:" {
7976 t.Errorf("subdomain without sites = %q", got)
8077 }
8178 }
+0, -24
......@@ -150,27 +150,3 @@ func (r *SshAuthHandler) PubkeyAuthHandler(conn ssh.ConnMetadata, key ssh.Public
150150
151151 return perms, nil
152152 }
153-
154-type FeatureFinder interface {
155- FindFeature(userID string, feature string) (*db.FeatureFlag, error)
156-}
157-
158-func FindPlusFF(dbpool FeatureFinder, cfg *ConfigSite, userID string) *db.FeatureFlag {
159- ff, _ := dbpool.FindFeature(userID, "plus")
160- // we have free tiers so users might not have a feature flag
161- // in which case we set sane defaults
162- if ff == nil {
163- ff = db.NewFeatureFlag(
164- userID,
165- "plus",
166- cfg.MaxSize,
167- cfg.MaxAssetSize,
168- cfg.MaxSpecialFileSize,
169- )
170- }
171- // this is jank
172- ff.Data.StorageMax = ff.FindStorageMax(cfg.MaxSize)
173- ff.Data.FileMax = ff.FindFileMax(cfg.MaxAssetSize)
174- ff.Data.SpecialFileMax = ff.FindSpecialFileMax(cfg.MaxSpecialFileSize)
175- return ff
176-}
+2, -2
......@@ -472,12 +472,12 @@ func (m *TunsPage) fetchEventLogs() {
472472 }
473473
474474 func (m *TunsPage) fetchTuns() {
475- tMap, err := fetch("tuns.sh", m.shared.Cfg.TunsSecret)
475+ tMap, err := fetch("tuns.sh", m.shared.TunsSecret)
476476 if err != nil {
477477 m.err = err
478478 return
479479 }
480- nMap, err := fetch("nue.tuns.sh", m.shared.Cfg.TunsSecret)
480+ nMap, err := fetch("nue.tuns.sh", m.shared.TunsSecret)
481481 if err != nil {
482482 m.err = err
483483 return
+2, -2
......@@ -24,7 +24,7 @@ const (
2424 type SharedModel struct {
2525 Logger *slog.Logger
2626 Session *pssh.SSHServerConnSession
27- Cfg *shared.ConfigSite
27+ TunsSecret string
2828 Dbpool db.DB
2929 PgsDB pgsdb.PgsDB
3030 User *db.User
......@@ -282,7 +282,7 @@ func initData(shrd *SharedModel) error {
282282 }
283283
284284 func FindUser(shrd *SharedModel) (*db.User, error) {
285- logger := shrd.Cfg.Logger
285+ logger := shrd.Logger
286286 var user *db.User
287287 usr := shrd.Session.User()
288288