repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
677655a
parent
97f9643
author
Eric Bower
date
2024-09-04 02:34:45 +0000 UTC
feat: pubsub service
27 files changed,  +1218, -14
M .env.example
+17, -0
 1@@ -145,3 +145,20 @@ AUTH_REAL_CERT_MOUNT=
 2 AUTH_DOMAIN=http://auth.dev.pico.sh:3006
 3 AUTH_ISSUER=pico.sh
 4 AUTH_WEB_PORT=3000
 5+
 6+PUBSUB_CADDYFILE=./caddy/Caddyfile
 7+PUBSUB_V4=
 8+PUBSUB_V6=
 9+PUBSUB_HTTP_V4=$PUBSUB_V4:80
10+PUBSUB_HTTP_V6=[$PUBSUB_V6]:80
11+PUBSUB_HTTPS_V4=$PUBSUB_V4:443
12+PUBSUB_HTTPS_V6=[$PUBSUB_V6]:443
13+PUBSUB_SSH_V4=$PUBSUB_V4:22
14+PUBSUB_SSH_V6=[$PUBSUB_V6]:22
15+PUBSUB_HOST=
16+PUBSUB_SSH_PORT=2222
17+PUBSUB_WEB_PORT=3000
18+PUBSUB_PROM_PORT=9222
19+PUBSUB_DOMAIN=pubsub.dev.pico.sh:3001
20+PUBSUB_PROTOCOL=http
21+PUBSUB_DEBUG=1
M .github/workflows/build.yml
+1, -1
1@@ -41,7 +41,7 @@ jobs:
2     needs: test
3     strategy:
4       matrix:
5-        APP: [prose, pastes, imgs, pgs, feeds]
6+        APP: [prose, pastes, imgs, pgs, feeds, pubsub]
7     steps:
8     - name: Checkout repo
9       uses: actions/checkout@v3
M Makefile
+2, -2
 1@@ -53,7 +53,7 @@ bp-%: bp-setup
 2 	$(DOCKER_BUILDX_BUILD) --build-arg "APP=$*" -t "ghcr.io/picosh/pico/$*-web:$(DOCKER_TAG)" --target release-web .
 3 .PHONY: bp-%
 4 
 5-bp-all: bp-prose bp-pastes bp-imgs bp-feeds bp-pgs bp-auth bp-bouncer
 6+bp-all: bp-prose bp-pastes bp-imgs bp-feeds bp-pgs bp-auth bp-bouncer bp-pubsub
 7 .PHONY: bp-all
 8 
 9 build-auth:
10@@ -69,7 +69,7 @@ build-%:
11 	go build -o "build/$*-ssh" "./cmd/$*/ssh"
12 .PHONY: build-%
13 
14-build: build-prose build-pastes build-imgs build-feeds build-pgs build-auth build-pico
15+build: build-prose build-pastes build-imgs build-feeds build-pgs build-auth build-pico build-pubsub
16 .PHONY: build
17 
18 store-clean:
M auth/api.go
+1, -1
1@@ -656,7 +656,7 @@ func StartApiServer() {
2 		Port:   shared.GetEnv("AUTH_WEB_PORT", "3000"),
3 	}
4 
5-	logger := shared.CreateLogger(true)
6+	logger := shared.CreateLogger()
7 	db := postgres.NewDB(cfg.DbURL, logger)
8 	defer db.Close()
9 
A cmd/pubsub/ssh/main.go
+7, -0
1@@ -0,0 +1,7 @@
2+package main
3+
4+import "github.com/picosh/pico/pubsub"
5+
6+func main() {
7+	pubsub.StartSshServer()
8+}
A cmd/pubsub/web/main.go
+7, -0
1@@ -0,0 +1,7 @@
2+package main
3+
4+import "github.com/picosh/pico/pubsub"
5+
6+func main() {
7+	pubsub.StartApiServer()
8+}
M feeds/config.go
+1, -1
1@@ -30,6 +30,6 @@ func NewConfigSite() *shared.ConfigSite {
2 		Space:       "feeds",
3 		AllowedExt:  []string{".txt"},
4 		HiddenPosts: []string{"_header.txt", "_readme.txt"},
5-		Logger:      shared.CreateLogger(debug == "1"),
6+		Logger:      shared.CreateLogger(),
7 	}
8 }
M imgs/config.go
+1, -1
1@@ -27,7 +27,7 @@ func NewConfigSite() *shared.ConfigSite {
2 		MinioPass:  minioPass,
3 		Space:      "imgs",
4 		AllowedExt: []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"},
5-		Logger:     shared.CreateLogger(debug == "1"),
6+		Logger:     shared.CreateLogger(),
7 	}
8 
9 	return &cfg
M imgs/ssh.go
+1, -1
1@@ -268,7 +268,7 @@ func StartSshServer() {
2 	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
3 	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
4 
5-	logger := shared.CreateLogger(false)
6+	logger := shared.CreateLogger()
7 	logger.Info("bootup", "registry", registryUrl, "minio", minioUrl)
8 	dbh := postgres.NewDB(dbUrl, logger)
9 	st, err := storage.NewStorageMinio(minioUrl, minioUser, minioPass)
M pastes/config.go
+1, -1
1@@ -26,6 +26,6 @@ func NewConfigSite() *shared.ConfigSite {
2 		MinioUser:  minioUser,
3 		MinioPass:  minioPass,
4 		Space:      "pastes",
5-		Logger:     shared.CreateLogger(debug == "1"),
6+		Logger:     shared.CreateLogger(),
7 	}
8 }
M pgs/config.go
+1, -3
 1@@ -8,7 +8,6 @@ var maxSize = uint64(25 * shared.MB)
 2 var maxAssetSize = int64(10 * shared.MB)
 3 
 4 func NewConfigSite() *shared.ConfigSite {
 5-	debug := shared.GetEnv("PGS_DEBUG", "0")
 6 	domain := shared.GetEnv("PGS_DOMAIN", "pgs.sh")
 7 	port := shared.GetEnv("PGS_WEB_PORT", "3000")
 8 	protocol := shared.GetEnv("PGS_PROTOCOL", "https")
 9@@ -23,7 +22,6 @@ func NewConfigSite() *shared.ConfigSite {
10 	}
11 
12 	cfg := shared.ConfigSite{
13-		Debug:        debug == "1",
14 		Secret:       secret,
15 		Domain:       domain,
16 		Port:         port,
17@@ -36,7 +34,7 @@ func NewConfigSite() *shared.ConfigSite {
18 		Space:        "pgs",
19 		MaxSize:      maxSize,
20 		MaxAssetSize: maxAssetSize,
21-		Logger:       shared.CreateLogger(debug == "1"),
22+		Logger:       shared.CreateLogger(),
23 	}
24 
25 	return &cfg
M pico/config.go
+1, -1
1@@ -10,6 +10,6 @@ func NewConfigSite() *shared.ConfigSite {
2 	return &shared.ConfigSite{
3 		DbURL:  dbURL,
4 		Space:  "pico",
5-		Logger: shared.CreateLogger(false),
6+		Logger: shared.CreateLogger(),
7 	}
8 }
M prose/config.go
+1, -1
1@@ -44,7 +44,7 @@ func NewConfigSite() *shared.ConfigSite {
2 			".ico",
3 		},
4 		HiddenPosts:  []string{"_readme.md", "_styles.css", "_footer.md", "_404.md"},
5-		Logger:       shared.CreateLogger(debug == "1"),
6+		Logger:       shared.CreateLogger(),
7 		MaxSize:      maxSize,
8 		MaxAssetSize: maxImgSize,
9 	}
A pubsub/api.go
+90, -0
 1@@ -0,0 +1,90 @@
 2+package pubsub
 3+
 4+import (
 5+	"fmt"
 6+	"net/http"
 7+	"os"
 8+
 9+	"github.com/picosh/pico/db/postgres"
10+	"github.com/picosh/pico/shared"
11+)
12+
13+func serveFile(file string, contentType string) http.HandlerFunc {
14+	return func(w http.ResponseWriter, r *http.Request) {
15+		logger := shared.GetLogger(r)
16+		cfg := shared.GetCfg(r)
17+
18+		contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
19+		if err != nil {
20+			logger.Error(err.Error())
21+			http.Error(w, "file not found", 404)
22+		}
23+		w.Header().Add("Content-Type", contentType)
24+
25+		_, err = w.Write(contents)
26+		if err != nil {
27+			logger.Error(err.Error())
28+			http.Error(w, "server error", 500)
29+		}
30+	}
31+}
32+
33+func createStaticRoutes() []shared.Route {
34+	return []shared.Route{
35+		shared.NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
36+		shared.NewRoute("GET", "/smol.css", serveFile("smol.css", "text/css")),
37+		shared.NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
38+		shared.NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
39+		shared.NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
40+		shared.NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
41+		shared.NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
42+		shared.NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
43+		shared.NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
44+	}
45+}
46+
47+func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
48+	routes := []shared.Route{
49+		shared.NewRoute("GET", "/", shared.CreatePageHandler("html/marketing.page.tmpl")),
50+		shared.NewRoute("GET", "/check", shared.CheckHandler),
51+	}
52+
53+	routes = append(
54+		routes,
55+		staticRoutes...,
56+	)
57+
58+	return routes
59+}
60+
61+func StartApiServer() {
62+	cfg := NewConfigSite()
63+	db := postgres.NewDB(cfg.DbURL, cfg.Logger)
64+	defer db.Close()
65+	logger := cfg.Logger
66+
67+	staticRoutes := createStaticRoutes()
68+
69+	if cfg.Debug {
70+		staticRoutes = shared.CreatePProfRoutes(staticRoutes)
71+	}
72+
73+	mainRoutes := createMainRoutes(staticRoutes)
74+	subdomainRoutes := staticRoutes
75+
76+	apiConfig := &shared.ApiConfig{
77+		Cfg:    cfg,
78+		Dbpool: db,
79+	}
80+	handler := shared.CreateServe(mainRoutes, subdomainRoutes, apiConfig)
81+	router := http.HandlerFunc(handler)
82+
83+	portStr := fmt.Sprintf(":%s", cfg.Port)
84+	logger.Info(
85+		"Starting server on port",
86+		"port", cfg.Port,
87+		"domain", cfg.Domain,
88+	)
89+
90+	logger.Error(http.ListenAndServe(portStr, router).Error())
91+}
A pubsub/cli.go
+159, -0
  1@@ -0,0 +1,159 @@
  2+package pubsub
  3+
  4+import (
  5+	"fmt"
  6+	"log/slog"
  7+	"strings"
  8+
  9+	"github.com/charmbracelet/ssh"
 10+	"github.com/charmbracelet/wish"
 11+	"github.com/google/uuid"
 12+	"github.com/picosh/pico/db"
 13+	"github.com/picosh/pico/shared"
 14+	"github.com/picosh/pico/shared/storage"
 15+	"github.com/picosh/pico/tui/common"
 16+	psub "github.com/picosh/pubsub"
 17+	"github.com/picosh/send/send/utils"
 18+)
 19+
 20+func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
 21+	var err error
 22+	key, err := shared.KeyText(s)
 23+	if err != nil {
 24+		return nil, fmt.Errorf("key not found")
 25+	}
 26+
 27+	user, err := dbpool.FindUserForKey(s.User(), key)
 28+	if err != nil {
 29+		return nil, err
 30+	}
 31+
 32+	if user.Name == "" {
 33+		return nil, fmt.Errorf("must have username set")
 34+	}
 35+
 36+	return user, nil
 37+}
 38+
 39+type Cmd struct {
 40+	User    *db.User
 41+	Session shared.CmdSession
 42+	Log     *slog.Logger
 43+	Dbpool  db.DB
 44+	Styles  common.Styles
 45+}
 46+
 47+func (c *Cmd) output(out string) {
 48+	_, _ = c.Session.Write([]byte(out + "\r\n"))
 49+}
 50+
 51+func (c *Cmd) error(err error) {
 52+	_, _ = fmt.Fprint(c.Session.Stderr(), err, "\r\n")
 53+	_ = c.Session.Exit(1)
 54+	_ = c.Session.Close()
 55+}
 56+
 57+func (c *Cmd) bail(err error) {
 58+	if err == nil {
 59+		return
 60+	}
 61+	c.Log.Error(err.Error())
 62+	c.error(err)
 63+}
 64+
 65+func (c *Cmd) help() {
 66+	helpStr := "Commands: [pub, sub, ls]\n"
 67+	c.output(helpStr)
 68+}
 69+
 70+func (c *Cmd) ls() error {
 71+	helpStr := "TODO\n"
 72+	c.output(helpStr)
 73+	return nil
 74+}
 75+
 76+type CliHandler struct {
 77+	DBPool      db.DB
 78+	Logger      *slog.Logger
 79+	Storage     storage.StorageServe
 80+	RegistryUrl string
 81+	PubSub      *psub.Cfg
 82+}
 83+
 84+func WishMiddleware(handler *CliHandler) wish.Middleware {
 85+	dbpool := handler.DBPool
 86+	log := handler.Logger
 87+	pubsub := handler.PubSub
 88+
 89+	return func(next ssh.Handler) ssh.Handler {
 90+		return func(sesh ssh.Session) {
 91+			user, err := getUser(sesh, dbpool)
 92+			if err != nil {
 93+				utils.ErrorHandler(sesh, err)
 94+				return
 95+			}
 96+
 97+			args := sesh.Command()
 98+
 99+			opts := Cmd{
100+				Session: sesh,
101+				User:    user,
102+				Log:     log,
103+				Dbpool:  dbpool,
104+			}
105+
106+			if len(args) == 0 {
107+				next(sesh)
108+				return
109+			}
110+
111+			cmd := strings.TrimSpace(args[0])
112+			if len(args) == 1 {
113+				if cmd == "help" {
114+					opts.help()
115+					return
116+				} else if cmd == "ls" {
117+					err := opts.ls()
118+					opts.bail(err)
119+					return
120+				} else {
121+					next(sesh)
122+					return
123+				}
124+			}
125+
126+			repoName := strings.TrimSpace(args[1])
127+			cmdArgs := args[2:]
128+			log.Info(
129+				"imgs middleware detected command",
130+				"args", args,
131+				"cmd", cmd,
132+				"repoName", repoName,
133+				"cmdArgs", cmdArgs,
134+			)
135+
136+			if cmd == "pub" {
137+				err = pubsub.PubSub.Pub(&psub.Msg{
138+					Name:   fmt.Sprintf("%s@%s", user.Name, repoName),
139+					Reader: sesh,
140+				})
141+				if err != nil {
142+					wish.Errorln(sesh, err)
143+				}
144+			} else if cmd == "sub" {
145+				err = pubsub.PubSub.Sub(&psub.Subscriber{
146+					ID:      uuid.NewString(),
147+					Name:    fmt.Sprintf("%s@%s", user.Name, repoName),
148+					Session: sesh,
149+					Chan:    make(chan error),
150+				})
151+				if err != nil {
152+					wish.Errorln(sesh, err)
153+				}
154+			} else {
155+				next(sesh)
156+				return
157+			}
158+		}
159+	}
160+}
A pubsub/config.go
+20, -0
 1@@ -0,0 +1,20 @@
 2+package pubsub
 3+
 4+import (
 5+	"github.com/picosh/pico/shared"
 6+)
 7+
 8+func NewConfigSite() *shared.ConfigSite {
 9+	domain := shared.GetEnv("PUBSUB_DOMAIN", "send.pico.sh")
10+	port := shared.GetEnv("PUBSUB_WEB_PORT", "3000")
11+	dbURL := shared.GetEnv("DATABASE_URL", "")
12+	protocol := shared.GetEnv("PUBSUB_PROTOCOL", "https")
13+
14+	return &shared.ConfigSite{
15+		Domain:   domain,
16+		Port:     port,
17+		Protocol: protocol,
18+		DbURL:    dbURL,
19+		Logger:   shared.CreateLogger(),
20+	}
21+}
A pubsub/html/base.layout.tmpl
+18, -0
 1@@ -0,0 +1,18 @@
 2+{{define "base"}}
 3+<!doctype html>
 4+<html lang="en">
 5+    <head>
 6+        <meta charset='utf-8'>
 7+        <meta name="viewport" content="width=device-width, initial-scale=1" />
 8+        <title>{{template "title" .}}</title>
 9+
10+        <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
11+
12+        <meta name="keywords" content="pastebin, paste, copy, snippets" />
13+        {{template "meta" .}}
14+
15+        <link rel="stylesheet" href="/smol.css" />
16+    </head>
17+    <body {{template "attrs" .}}>{{template "body" .}}</body>
18+</html>
19+{{end}}
A pubsub/html/marketing-footer.partial.tmpl
+6, -0
1@@ -0,0 +1,6 @@
2+{{define "marketing-footer"}}
3+<footer>
4+    <hr />
5+    <p class="font-italic">Built and maintained by <a href="https://pico.sh">pico.sh</a>.</p>
6+</footer>
7+{{end}}
A pubsub/html/marketing.page.tmpl
+38, -0
 1@@ -0,0 +1,38 @@
 2+{{template "base" .}}
 3+
 4+{{define "title"}}{{.Site.Domain}} -- pubsub using ssh{{end}}
 5+
 6+{{define "meta"}}
 7+<meta name="description" content="pubsub using ssh" />
 8+
 9+<meta property="og:type" content="website">
10+<meta property="og:site_name" content="{{.Site.Domain}}">
11+<meta property="og:url" content="https://{{.Site.Domain}}">
12+<meta property="og:title" content="{{.Site.Domain}}">
13+<meta property="og:description" content="pubsub using ssh">
14+
15+<meta name="twitter:card" content="summary" />
16+<meta property="twitter:url" content="https://{{.Site.Domain}}">
17+<meta property="twitter:title" content="{{.Site.Domain}}">
18+<meta property="twitter:description" content="pubsub using ssh">
19+<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
20+<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
21+
22+<meta property="og:image:width" content="300" />
23+<meta property="og:image:height" content="300" />
24+<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
25+<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
26+{{end}}
27+
28+{{define "attrs"}}{{end}}
29+
30+{{define "body"}}
31+<header class="text-center">
32+    <h1 class="text-2xl font-bold">{{.Site.Domain}}</h1>
33+    <p class="text-lg">pubsub using ssh</p>
34+    <pre>ssh {{.Site.Domain}} sub mykey</pre>
35+    <pre>echo "hello world!" | ssh {{.Site.Domain}} pub mykey</pre>
36+</header>
37+
38+{{template "marketing-footer" .}}
39+{{end}}
A pubsub/public/apple-touch-icon.png
+0, -0
A pubsub/public/card.png
+0, -0
A pubsub/public/favicon-16x16.png
+0, -0
A pubsub/public/favicon.ico
+0, -0
A pubsub/public/robots.txt
+2, -0
1@@ -0,0 +1,2 @@
2+User-agent: *
3+Allow: /
A pubsub/public/smol.css
+768, -0
  1@@ -0,0 +1,768 @@
  2+*,
  3+::before,
  4+::after {
  5+  box-sizing: border-box;
  6+}
  7+
  8+::-moz-focus-inner {
  9+  border-style: none;
 10+  padding: 0;
 11+}
 12+:-moz-focusring {
 13+  outline: 1px dotted ButtonText;
 14+}
 15+:-moz-ui-invalid {
 16+  box-shadow: none;
 17+}
 18+
 19+@media (prefers-color-scheme: light) {
 20+  :root {
 21+    --main-hue: 250;
 22+    --white: #2e3f53;
 23+    --white-light: #cfe0f4;
 24+    --white-dark: #6c6a6a;
 25+    --code: #52576f;
 26+    --pre: #e1e7ee;
 27+    --bg-color: #f4f4f4;
 28+    --text-color: #24292f;
 29+    --link-color: #005cc5;
 30+    --visited: #6f42c1;
 31+    --blockquote: #005cc5;
 32+    --blockquote-bg: #cfe0f4;
 33+    --hover: #c11e7a;
 34+    --grey: #ccc;
 35+    --grey-light: #6a708e;
 36+    --shadow: #e8e8e8;
 37+  }
 38+}
 39+
 40+@media (prefers-color-scheme: dark) {
 41+  :root {
 42+    --main-hue: 250;
 43+    --white: #f2f2f2;
 44+    --white-light: #f2f2f2;
 45+    --white-dark: #e8e8e8;
 46+    --code: #414558;
 47+    --pre: #252525;
 48+    --bg-color: #282a36;
 49+    --text-color: #f2f2f2;
 50+    --link-color: #8be9fd;
 51+    --visited: #bd93f9;
 52+    --blockquote: #bd93f9;
 53+    --blockquote-bg: #353548;
 54+    --hover: #ff80bf;
 55+    --grey: #414558;
 56+    --grey-light: #6a708e;
 57+    --shadow: #252525;
 58+  }
 59+}
 60+
 61+html {
 62+  background-color: var(--bg-color);
 63+  color: var(--text-color);
 64+  font-size: 18px;
 65+  line-height: 1.5;
 66+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
 67+    Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
 68+    sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 69+  -webkit-text-size-adjust: 100%;
 70+  -moz-tab-size: 4;
 71+  -o-tab-size: 4;
 72+  tab-size: 4;
 73+}
 74+
 75+body {
 76+  margin: 0 auto;
 77+}
 78+
 79+img {
 80+  max-width: 100%;
 81+  height: auto;
 82+}
 83+
 84+b,
 85+strong {
 86+  font-weight: bold;
 87+}
 88+
 89+code,
 90+kbd,
 91+samp,
 92+pre {
 93+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
 94+    monospace;
 95+}
 96+
 97+code,
 98+kbd,
 99+samp {
100+  border: 2px solid var(--code);
101+}
102+
103+pre > code {
104+  background-color: inherit;
105+  padding: 0;
106+  border: none;
107+}
108+
109+code {
110+  font-size: 90%;
111+  border-radius: 0.3rem;
112+  padding: 0.1rem 0.3rem;
113+}
114+
115+pre {
116+  font-size: 14px;
117+  border-radius: 5px;
118+  padding: 1rem;
119+  margin: 1rem 0;
120+  overflow-x: auto;
121+  background-color: var(--pre) !important;
122+}
123+
124+small {
125+  font-size: 0.8rem;
126+}
127+
128+summary {
129+  display: list-item;
130+  cursor: pointer;
131+}
132+
133+h1,
134+h2,
135+h3,
136+h4 {
137+  margin: 0;
138+  padding: 0.5rem 0 0 0;
139+  border: 0;
140+  font-style: normal;
141+  font-weight: inherit;
142+  font-size: inherit;
143+}
144+
145+path {
146+  fill: var(--text-color);
147+  stroke: var(--text-color);
148+}
149+
150+hr {
151+  color: inherit;
152+  border: 0;
153+  margin: 0;
154+  height: 2px;
155+  background: var(--grey);
156+  margin: 1rem auto;
157+  text-align: center;
158+}
159+
160+a {
161+  text-decoration: none;
162+  color: var(--link-color);
163+}
164+
165+a:hover,
166+a:visited:hover {
167+  text-decoration: underline;
168+  color: var(--hover);
169+}
170+
171+a:visited {
172+  color: var(--visited);
173+}
174+
175+a.link-grey {
176+  text-decoration: underline;
177+  color: var(--white);
178+}
179+
180+a.link-grey:visited {
181+  color: var(--white);
182+}
183+
184+section {
185+  margin-bottom: 1.4rem;
186+}
187+
188+section:last-child {
189+  margin-bottom: 0;
190+}
191+
192+header {
193+  margin: 1rem auto;
194+}
195+
196+p {
197+  margin: 0.5rem 0;
198+}
199+
200+article {
201+  overflow-wrap: break-word;
202+}
203+
204+blockquote {
205+  border-left: 5px solid var(--blockquote);
206+  background-color: var(--blockquote-bg);
207+  padding: 0.5rem 0.75rem;
208+  margin: 0.5rem 0;
209+}
210+
211+blockquote > p {
212+  margin: 0;
213+}
214+
215+blockquote code {
216+  border: 1px solid var(--blockquote);
217+}
218+
219+ul,
220+ol {
221+  padding: 0 0 0 1rem;
222+  list-style-position: outside;
223+}
224+
225+ul[style*="list-style-type: none;"] {
226+  padding: 0;
227+}
228+
229+li {
230+  margin: 0.5rem 0;
231+}
232+
233+li > pre {
234+  padding: 0;
235+}
236+
237+footer {
238+  text-align: center;
239+  margin-bottom: 4rem;
240+}
241+
242+dt {
243+  font-weight: bold;
244+}
245+
246+dd {
247+  margin-left: 0;
248+}
249+
250+dd:not(:last-child) {
251+  margin-bottom: 0.5rem;
252+}
253+
254+figure {
255+  margin: 0;
256+}
257+
258+.container {
259+  max-width: 50em;
260+  width: 100%;
261+}
262+
263+.container-sm {
264+  max-width: 40em;
265+  width: 100%;
266+}
267+
268+.container-center {
269+  width: 100%;
270+  height: 100%;
271+  display: flex;
272+  justify-content: center;
273+}
274+
275+.mono {
276+  font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo,
277+    monospace;
278+}
279+
280+.link-alt-adj,
281+.link-alt-adj:visited,
282+.link-alt-adj:visited:hover,
283+.link-alt-adj:hover {
284+  color: var(--link-color);
285+  text-decoration: none;
286+}
287+
288+.link-alt-adj:visited:hover,
289+.link-alt-adj:hover {
290+  text-decoration: underline;
291+}
292+
293+.link-alt-hover,
294+.link-alt-hover:visited,
295+.link-alt-hover:visited:hover,
296+.link-alt-hover:hover {
297+  color: var(--hover);
298+  text-decoration: none;
299+}
300+
301+.link-alt-hover:visited:hover,
302+.link-alt-hover:hover {
303+  text-decoration: underline;
304+}
305+
306+.link-alt,
307+.link-alt:visited,
308+.link-alt:visited:hover,
309+.link-alt:hover {
310+  color: var(--white);
311+  text-decoration: none;
312+}
313+
314+.link-alt:visited:hover,
315+.link-alt:hover {
316+  text-decoration: underline;
317+}
318+
319+.text-3xl {
320+  font-size: 2.5rem;
321+}
322+
323+.text-2xl {
324+  font-size: 1.9rem;
325+  line-height: 1.15;
326+}
327+
328+.text-xl {
329+  font-size: 1.55rem;
330+  line-height: 1.15;
331+}
332+
333+.text-lg {
334+  font-size: 1.35rem;
335+  line-height: 1.15;
336+}
337+
338+.text-md {
339+  font-size: 1.15rem;
340+  line-height: 1.15;
341+}
342+
343+.text-sm {
344+  font-size: 0.875rem;
345+}
346+
347+.text-xs {
348+  font-size: 0.775rem;
349+}
350+
351+.cursor-pointer {
352+  cursor: pointer;
353+}
354+
355+.w-full {
356+  width: 100%;
357+}
358+
359+.h-full {
360+  height: 100%;
361+}
362+
363+.border {
364+  border: 2px solid var(--grey-light);
365+}
366+
367+.text-left {
368+  text-align: left;
369+}
370+
371+.text-center {
372+  text-align: center;
373+}
374+
375+.text-underline {
376+  border-bottom: 3px solid var(--text-color);
377+  padding-bottom: 3px;
378+}
379+
380+.text-hdr {
381+  color: var(--hover);
382+}
383+
384+.text-underline-hdr {
385+  border-bottom: 3px solid var(--hover);
386+  padding-bottom: 3px;
387+}
388+
389+.font-bold {
390+  font-weight: bold;
391+}
392+
393+.font-italic {
394+  font-style: italic;
395+}
396+
397+.inline {
398+  display: inline;
399+}
400+
401+.inline-block {
402+  display: inline-block;
403+}
404+
405+.max-w-half {
406+  max-width: 50%;
407+}
408+
409+.h-screen {
410+  height: 100vh;
411+}
412+
413+.w-screen {
414+  width: 100vw;
415+}
416+
417+.flex {
418+  display: flex;
419+}
420+
421+.flex-col {
422+  flex-direction: column;
423+}
424+
425+.items-center {
426+  align-items: center;
427+}
428+
429+.m-0 {
430+  margin: 0;
431+}
432+
433+.mt {
434+  margin-top: 0.5rem;
435+}
436+
437+.mt-2 {
438+  margin-top: 1rem;
439+}
440+
441+.mt-4 {
442+  margin-top: 2rem;
443+}
444+
445+.mt-8 {
446+  margin-top: 4rem;
447+}
448+
449+.mb {
450+  margin-bottom: 0.5rem;
451+}
452+
453+.mb-2 {
454+  margin-bottom: 1rem;
455+}
456+
457+.mb-4 {
458+  margin-bottom: 2rem;
459+}
460+
461+.mb-8 {
462+  margin-bottom: 4rem;
463+}
464+
465+.mb-16 {
466+  margin-bottom: 8rem;
467+}
468+
469+.mr {
470+  margin-right: 0.5rem;
471+}
472+
473+.ml-sm {
474+  margin-left: 0.25rem;
475+}
476+
477+.ml {
478+  margin-left: 0.5rem;
479+}
480+
481+.pt-0 {
482+  padding-top: 0;
483+}
484+
485+.my {
486+  margin-top: 0.5rem;
487+  margin-bottom: 0.5rem;
488+}
489+
490+.my-2 {
491+  margin-top: 1rem;
492+  margin-bottom: 1rem;
493+}
494+
495+.my-4 {
496+  margin-top: 2rem;
497+  margin-bottom: 2rem;
498+}
499+
500+.my-8 {
501+  margin-top: 4rem;
502+  margin-bottom: 4rem;
503+}
504+
505+.mx {
506+  margin-left: 0.5rem;
507+  margin-right: 0.5rem;
508+}
509+
510+.mx-2 {
511+  margin-left: 1rem;
512+  margin-right: 1rem;
513+}
514+
515+.m-1 {
516+  margin: 0.5rem;
517+}
518+
519+.p-1 {
520+  padding: 0.5rem;
521+}
522+
523+.p-0 {
524+  padding: 0;
525+}
526+
527+.px-2 {
528+  padding-left: 1rem;
529+  padding-right: 1rem;
530+}
531+
532+.px-4 {
533+  padding-left: 2rem;
534+  padding-right: 2rem;
535+}
536+
537+.py {
538+  padding-top: 0.5rem;
539+  padding-bottom: 0.5rem;
540+}
541+
542+.py-2 {
543+  padding-top: 1rem;
544+  padding-bottom: 1rem;
545+}
546+
547+.py-4 {
548+  padding-top: 2rem;
549+  padding-bottom: 2rem;
550+}
551+
552+.py-8 {
553+  padding-top: 4rem;
554+  padding-bottom: 4rem;
555+}
556+
557+.justify-between {
558+  justify-content: space-between;
559+}
560+
561+.justify-center {
562+  justify-content: center;
563+}
564+
565+.gap {
566+  gap: 0.5rem;
567+}
568+
569+.gap-2 {
570+  gap: 1rem;
571+}
572+
573+.group {
574+  display: flex;
575+  flex-direction: column;
576+  gap: 0.5rem;
577+}
578+
579+.group-2 {
580+  display: flex;
581+  flex-direction: column;
582+  gap: 1rem;
583+}
584+
585+.group-h {
586+  display: flex;
587+  gap: 0.5rem;
588+  align-items: center;
589+}
590+
591+.flex-1 {
592+  flex: 1;
593+}
594+
595+.items-end {
596+  align-items: end;
597+}
598+
599+.items-start {
600+  align-items: start;
601+}
602+
603+.justify-end {
604+  justify-content: end;
605+}
606+
607+.font-grey-light {
608+  color: var(--grey-light);
609+}
610+
611+.hidden {
612+  display: none;
613+}
614+
615+.align-right {
616+  text-align: right;
617+}
618+
619+/* ==== MARKDOWN ==== */
620+
621+.md h1,
622+.md h2,
623+.md h3,
624+.md h4 {
625+  padding: 0;
626+  margin: 1.5rem 0 0.9rem 0;
627+  font-weight: bold;
628+}
629+
630+.md h1 a,
631+.md h2 a,
632+.md h3 a,
633+.md h4 a {
634+  color: var(--grey-light);
635+  text-decoration: none;
636+}
637+
638+.md h1 {
639+  font-size: 1.6rem;
640+  line-height: 1.15;
641+  border-bottom: 2px solid var(--grey);
642+  padding-bottom: 0.7rem;
643+}
644+
645+.md h2 {
646+  font-size: 1.3rem;
647+  line-height: 1.15;
648+  color: var(--white-dark);
649+}
650+
651+.md h3 {
652+  font-size: 1.2rem;
653+  color: var(--white-dark);
654+}
655+
656+.md h4 {
657+  font-size: 1rem;
658+  color: var(--white-dark);
659+}
660+
661+/* ==== HELPERS ==== */
662+
663+.logo-header {
664+  line-height: 1;
665+  display: inline-block;
666+  background-color: #FF79C6;
667+  background-image: linear-gradient(to right, #FF5555, #FF79C6, #F8F859);
668+  color: transparent;
669+  background-clip: text;
670+  border: 3px solid #FF79C6;
671+  padding: 8px 10px 10px 10px;
672+  border-radius: 10px;
673+  box-shadow: 0px 5px 0px 0px var(--shadow);
674+  background-size: 100%;
675+  -webkit-background-clip: text;
676+  -moz-background-clip: text;
677+  -webkit-text-fill-color: transparent;
678+  -moz-text-fill-color: transparent;
679+}
680+
681+.btn {
682+  border: 2px solid var(--link-color);
683+  color: var(--link-color);
684+  padding: 0.4rem 1rem;
685+  font-weight: bold;
686+  display: inline-block;
687+}
688+
689+.btn-link,
690+.btn-link:visited {
691+  border: 2px solid var(--link-color);
692+  color: var(--link-color);
693+  padding: 0.4rem 1rem;
694+  text-decoration: none;
695+  font-weight: bold;
696+  display: inline-block;
697+}
698+
699+.btn-link:visited:hover,
700+.btn-link:hover {
701+  border: 2px solid var(--hover);
702+}
703+
704+.btn-link-alt,
705+.btn-link-alt:visited {
706+  border: 2px solid var(--white);
707+  color: var(--white);
708+}
709+
710+.box {
711+  border: 2px solid var(--grey-light);
712+  padding: 0.5rem 0.75rem;
713+}
714+
715+.box-sm {
716+  border: 2px solid var(--grey-light);
717+  padding: 0.15rem 0.35rem;
718+}
719+
720+.box-alert {
721+  border: 2px solid var(--hover);
722+  padding: 0.5rem 0.75rem;
723+}
724+
725+.box-sm-alert {
726+  border: 2px solid var(--hover);
727+  padding: 0.15rem 0.35rem;
728+}
729+
730+.list-none {
731+  list-style-type: none;
732+}
733+
734+.list-disc {
735+  list-style-type: disc;
736+}
737+
738+.list-decimal {
739+  list-style-type: decimal;
740+}
741+
742+.pill {
743+  border: 1px solid var(--link-color);
744+  color: var(--link-color);
745+}
746+
747+.pill-alert {
748+  border: 1px solid var(--hover);
749+  color: var(--hover);
750+}
751+
752+.pill-info {
753+  border: 1px solid var(--visited);
754+  color: var(--visited);
755+}
756+
757+@media only screen and (max-width: 40em) {
758+  body {
759+    padding: 0 1rem;
760+  }
761+
762+  header {
763+    margin: 0;
764+  }
765+
766+  .flex-collapse {
767+    flex-direction: column;
768+  }
769+}
A pubsub/ssh.go
+74, -0
 1@@ -0,0 +1,74 @@
 2+package pubsub
 3+
 4+import (
 5+	"context"
 6+	"fmt"
 7+	"os"
 8+	"os/signal"
 9+	"syscall"
10+	"time"
11+
12+	"github.com/charmbracelet/promwish"
13+	"github.com/charmbracelet/wish"
14+	"github.com/picosh/pico/db/postgres"
15+	"github.com/picosh/pico/filehandlers/util"
16+	"github.com/picosh/pico/shared"
17+	wsh "github.com/picosh/pico/wish"
18+	psub "github.com/picosh/pubsub"
19+)
20+
21+func StartSshServer() {
22+	host := shared.GetEnv("PUBSUB_HOST", "0.0.0.0")
23+	port := shared.GetEnv("PUBSUB_SSH_PORT", "2222")
24+	promPort := shared.GetEnv("PUBSUB_PROM_PORT", "9222")
25+	cfg := NewConfigSite()
26+	logger := cfg.Logger
27+	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
28+	defer dbh.Close()
29+
30+	pubsub := &psub.Cfg{
31+		Logger: logger,
32+		PubSub: &psub.PubSubMulticast{
33+			Logger: logger,
34+		},
35+	}
36+
37+	handler := &CliHandler{
38+		Logger: logger,
39+		DBPool: dbh,
40+		PubSub: pubsub,
41+	}
42+
43+	sshAuth := util.NewSshAuthHandler(dbh, logger, cfg)
44+	s, err := wish.NewServer(
45+		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
46+		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
47+		wish.WithPublicKeyAuth(sshAuth.PubkeyAuthHandler),
48+		wish.WithMiddleware(WishMiddleware(handler)),
49+		wish.WithMiddleware(
50+			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pubsub-ssh"),
51+			wsh.LogMiddleware(logger),
52+		),
53+	)
54+	if err != nil {
55+		logger.Error(err.Error())
56+		return
57+	}
58+
59+	done := make(chan os.Signal, 1)
60+	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
61+	logger.Info("Starting SSH server", "host", host, "port", port)
62+	go func() {
63+		if err = s.ListenAndServe(); err != nil {
64+			logger.Error(err.Error())
65+		}
66+	}()
67+
68+	<-done
69+	logger.Info("Stopping SSH server")
70+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
71+	defer func() { cancel() }()
72+	if err := s.Shutdown(ctx); err != nil {
73+		logger.Error(err.Error())
74+	}
75+}
M shared/config.go
+1, -1
1@@ -261,7 +261,7 @@ func (c *ConfigSite) AssetURL(username, projectName, fpath string) string {
2 	)
3 }
4 
5-func CreateLogger(debug bool) *slog.Logger {
6+func CreateLogger() *slog.Logger {
7 	opts := &slog.HandlerOptions{
8 		AddSource: true,
9 	}