this repo has no description

legit: setup db stuff and keys page

+8 -4
cmd/legit/main.go
··· 3 3 import ( 4 4 "flag" 5 5 "fmt" 6 - "html/template" 7 6 "log" 7 + "log/slog" 8 8 "net/http" 9 - "path/filepath" 9 + "os" 10 10 11 11 "github.com/icyphox/bild/legit/config" 12 12 "github.com/icyphox/bild/legit/routes" ··· 16 16 var cfg string 17 17 flag.StringVar(&cfg, "config", "./config.yaml", "path to config file") 18 18 flag.Parse() 19 + 20 + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil))) 19 21 20 22 c, err := config.Read(cfg) 21 23 if err != nil { 22 24 log.Fatal(err) 23 25 } 24 26 25 - t := template.Must(template.ParseGlob(filepath.Join(c.Dirs.Templates, "*"))) 27 + mux, err := routes.Setup(c) 28 + if err != nil { 29 + log.Fatal(err) 30 + } 26 31 27 - mux := routes.Handlers(c, t) 28 32 addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port) 29 33 log.Println("starting server on", addr) 30 34 log.Fatal(http.ListenAndServe(addr, mux))
+1
config.yaml
··· 18 18 name: github.com/icyphox/bild 19 19 host: 0.0.0.0 20 20 port: 5555 21 + dbpath: bild.db
+1
go.mod
··· 10 10 github.com/dustin/go-humanize v1.0.1 11 11 github.com/go-chi/chi/v5 v5.2.0 12 12 github.com/go-git/go-git/v5 v5.12.0 13 + github.com/mattn/go-sqlite3 v1.14.24 13 14 github.com/microcosm-cc/bluemonday v1.0.27 14 15 github.com/russross/blackfriday/v2 v2.1.0 15 16 gopkg.in/yaml.v3 v3.0.1
+2
go.sum
··· 78 78 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 79 79 github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 80 80 github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 81 + github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 82 + github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 81 83 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 82 84 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 83 85 github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
+4 -3
legit/config/config.go
··· 26 26 SyntaxHighlight string `yaml:"syntaxHighlight"` 27 27 } `yaml:"meta"` 28 28 Server struct { 29 - Name string `yaml:"name,omitempty"` 30 - Host string `yaml:"host"` 31 - Port int `yaml:"port"` 29 + Name string `yaml:"name,omitempty"` 30 + Host string `yaml:"host"` 31 + Port int `yaml:"port"` 32 + DBPath string `yaml:"dbpath"` 32 33 } `yaml:"server"` 33 34 } 34 35
+32
legit/db/init.go
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + 6 + _ "github.com/mattn/go-sqlite3" 7 + ) 8 + 9 + type DB struct { 10 + db *sql.DB 11 + } 12 + 13 + func Setup(dbPath string) (*DB, error) { 14 + db, err := sql.Open("sqlite3", dbPath) 15 + if err != nil { 16 + return nil, err 17 + } 18 + 19 + _, err = db.Exec(` 20 + create table if not exists public_keys ( 21 + id integer primary key autoincrement, 22 + did text not null, 23 + name text not null, 24 + key text not null, 25 + unique(did, name, key)) 26 + `) 27 + if err != nil { 28 + return nil, err 29 + } 30 + 31 + return &DB{db: db}, nil 32 + }
+23
legit/db/pubkeys.go
··· 1 + package db 2 + 3 + func (d *DB) AddPublicKey(did, name, key string) error { 4 + query := `insert into public_keys (did, name, key) values (?, ?, ?)` 5 + _, err := d.db.Exec(query, did, name, key) 6 + return err 7 + } 8 + 9 + func (d *DB) RemovePublicKey(did string) error { 10 + query := `delete from public_keys where did = ?` 11 + _, err := d.db.Exec(query, did) 12 + return err 13 + } 14 + 15 + func (d *DB) GetPublicKey(did string) (string, error) { 16 + var key string 17 + query := `select key from public_keys where did = ?` 18 + err := d.db.QueryRow(query, did).Scan(&key) 19 + if err != nil { 20 + return "", err 21 + } 22 + return key, nil 23 + }
+2 -2
legit/routes/git.go
··· 10 10 "github.com/icyphox/bild/legit/git/service" 11 11 ) 12 12 13 - func (d *deps) InfoRefs(w http.ResponseWriter, r *http.Request) { 13 + func (d *Handle) InfoRefs(w http.ResponseWriter, r *http.Request) { 14 14 name := uniqueName(r) 15 15 name = filepath.Clean(name) 16 16 ··· 31 31 } 32 32 } 33 33 34 - func (d *deps) UploadPack(w http.ResponseWriter, r *http.Request) { 34 + func (d *Handle) UploadPack(w http.ResponseWriter, r *http.Request) { 35 35 name := uniqueName(r) 36 36 name = filepath.Clean(name) 37 37
+41 -20
legit/routes/handler.go
··· 1 1 package routes 2 2 3 3 import ( 4 + "fmt" 4 5 "html/template" 5 6 "net/http" 7 + "path/filepath" 6 8 7 9 "github.com/go-chi/chi/v5" 8 10 "github.com/icyphox/bild/legit/config" 11 + "github.com/icyphox/bild/legit/db" 9 12 ) 10 13 11 14 // Checks for gitprotocol-http(5) specific smells; if found, passes 12 15 // the request on to the git http service, else render the web frontend. 13 - func (d *deps) Multiplex(w http.ResponseWriter, r *http.Request) { 16 + func (h *Handle) Multiplex(w http.ResponseWriter, r *http.Request) { 14 17 path := chi.URLParam(r, "*") 15 18 16 19 if r.URL.RawQuery == "service=git-receive-pack" { ··· 22 25 if path == "info/refs" && 23 26 r.URL.RawQuery == "service=git-upload-pack" && 24 27 r.Method == "GET" { 25 - d.InfoRefs(w, r) 28 + h.InfoRefs(w, r) 26 29 } else if path == "git-upload-pack" && r.Method == "POST" { 27 - d.UploadPack(w, r) 30 + h.UploadPack(w, r) 28 31 } else if r.Method == "GET" { 29 - d.RepoIndex(w, r) 32 + h.RepoIndex(w, r) 30 33 } 31 34 } 32 35 33 - func Handlers(c *config.Config, t *template.Template) http.Handler { 36 + func Setup(c *config.Config) (http.Handler, error) { 34 37 r := chi.NewRouter() 35 - d := deps{c, t} 38 + t := template.Must(template.ParseGlob(filepath.Join(c.Dirs.Templates, "*"))) 39 + db, err := db.Setup(c.Server.DBPath) 40 + 41 + if err != nil { 42 + return nil, fmt.Errorf("failed to setup db: %w", err) 43 + } 44 + 45 + h := Handle{ 46 + c: c, 47 + t: t, 48 + db: db, 49 + } 50 + 51 + r.Get("/login", h.Login) 52 + r.Get("/static/{file}", h.ServeStatic) 36 53 37 - r.Get("/login", d.Login) 38 - r.Get("/static/{file}", d.ServeStatic) 54 + r.Route("/settings", func(r chi.Router) { 55 + r.Get("/keys", h.Keys) 56 + r.Put("/keys", h.Keys) 57 + }) 39 58 40 59 r.Route("/@{user}", func(r chi.Router) { 41 - r.Get("/", d.Index) 60 + r.Get("/", h.Index) 61 + 62 + // Repo routes 42 63 r.Route("/{name}", func(r chi.Router) { 43 - r.Get("/", d.Multiplex) 44 - r.Post("/", d.Multiplex) 64 + r.Get("/", h.Multiplex) 65 + r.Post("/", h.Multiplex) 45 66 46 67 r.Route("/tree/{ref}", func(r chi.Router) { 47 - r.Get("/*", d.RepoTree) 68 + r.Get("/*", h.RepoTree) 48 69 }) 49 70 50 71 r.Route("/blob/{ref}", func(r chi.Router) { 51 - r.Get("/*", d.FileContent) 72 + r.Get("/*", h.FileContent) 52 73 }) 53 74 54 - r.Get("/log/{ref}", d.Log) 55 - r.Get("/archive/{file}", d.Archive) 56 - r.Get("/commit/{ref}", d.Diff) 57 - r.Get("/refs/", d.Refs) 75 + r.Get("/log/{ref}", h.Log) 76 + r.Get("/archive/{file}", h.Archive) 77 + r.Get("/commit/{ref}", h.Diff) 78 + r.Get("/refs/", h.Refs) 58 79 59 80 // Catch-all routes 60 - r.Get("/*", d.Multiplex) 61 - r.Post("/*", d.Multiplex) 81 + r.Get("/*", h.Multiplex) 82 + r.Post("/*", h.Multiplex) 62 83 }) 63 84 }) 64 85 65 - return r 86 + return r, nil 66 87 }
+98 -73
legit/routes/routes.go
··· 16 16 "github.com/dustin/go-humanize" 17 17 "github.com/go-chi/chi/v5" 18 18 "github.com/icyphox/bild/legit/config" 19 + "github.com/icyphox/bild/legit/db" 19 20 "github.com/icyphox/bild/legit/git" 20 21 "github.com/russross/blackfriday/v2" 21 22 ) 22 23 23 - type deps struct { 24 - c *config.Config 25 - t *template.Template 24 + type Handle struct { 25 + c *config.Config 26 + t *template.Template 27 + db *db.DB 26 28 } 27 29 28 - func (d *deps) Index(w http.ResponseWriter, r *http.Request) { 30 + func (h *Handle) Index(w http.ResponseWriter, r *http.Request) { 29 31 user := chi.URLParam(r, "user") 30 - path := filepath.Join(d.c.Repo.ScanPath, user) 32 + path := filepath.Join(h.c.Repo.ScanPath, user) 31 33 dirs, err := os.ReadDir(path) 32 34 if err != nil { 33 - d.Write500(w) 35 + h.Write500(w) 34 36 log.Printf("reading scan path: %s", err) 35 37 return 36 38 } ··· 44 46 45 47 for _, dir := range dirs { 46 48 name := dir.Name() 47 - if !dir.IsDir() || d.isIgnored(name) || d.isUnlisted(name) { 49 + if !dir.IsDir() || h.isIgnored(name) || h.isUnlisted(name) { 48 50 continue 49 51 } 50 52 ··· 56 58 57 59 c, err := gr.LastCommit() 58 60 if err != nil { 59 - d.Write500(w) 61 + h.Write500(w) 60 62 log.Println(err) 61 63 return 62 64 } ··· 75 77 }) 76 78 77 79 data := make(map[string]interface{}) 78 - data["meta"] = d.c.Meta 80 + data["meta"] = h.c.Meta 79 81 data["info"] = infos 80 82 81 - if err := d.t.ExecuteTemplate(w, "index", data); err != nil { 83 + if err := h.t.ExecuteTemplate(w, "index", data); err != nil { 82 84 log.Println(err) 83 85 return 84 86 } 85 87 } 86 88 87 - func (d *deps) RepoIndex(w http.ResponseWriter, r *http.Request) { 89 + func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) { 88 90 name := uniqueName(r) 89 - if d.isIgnored(name) { 90 - d.Write404(w) 91 + if h.isIgnored(name) { 92 + h.Write404(w) 91 93 return 92 94 } 93 95 94 96 name = filepath.Clean(name) 95 - path := filepath.Join(d.c.Repo.ScanPath, name) 97 + path := filepath.Join(h.c.Repo.ScanPath, name) 96 98 97 99 fmt.Println(path) 98 100 gr, err := git.Open(path, "") 99 101 if err != nil { 100 - d.Write404(w) 102 + h.Write404(w) 101 103 return 102 104 } 103 105 commits, err := gr.Commits() 104 106 if err != nil { 105 - d.Write500(w) 107 + h.Write500(w) 106 108 log.Println(err) 107 109 return 108 110 } 109 111 110 112 var readmeContent template.HTML 111 - for _, readme := range d.c.Repo.Readme { 113 + for _, readme := range h.c.Repo.Readme { 112 114 ext := filepath.Ext(readme) 113 115 content, _ := gr.FileContent(readme) 114 116 if len(content) > 0 { ··· 134 136 log.Printf("no readme found for %s", name) 135 137 } 136 138 137 - mainBranch, err := gr.FindMainBranch(d.c.Repo.MainBranch) 139 + mainBranch, err := gr.FindMainBranch(h.c.Repo.MainBranch) 138 140 if err != nil { 139 - d.Write500(w) 141 + h.Write500(w) 140 142 log.Println(err) 141 143 return 142 144 } ··· 152 154 data["readme"] = readmeContent 153 155 data["commits"] = commits 154 156 data["desc"] = getDescription(path) 155 - data["servername"] = d.c.Server.Name 156 - data["meta"] = d.c.Meta 157 + data["servername"] = h.c.Server.Name 158 + data["meta"] = h.c.Meta 157 159 data["gomod"] = isGoModule(gr) 158 160 159 - if err := d.t.ExecuteTemplate(w, "repo", data); err != nil { 161 + if err := h.t.ExecuteTemplate(w, "repo", data); err != nil { 160 162 log.Println(err) 161 163 return 162 164 } ··· 164 166 return 165 167 } 166 168 167 - func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request) { 169 + func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) { 168 170 name := uniqueName(r) 169 - if d.isIgnored(name) { 170 - d.Write404(w) 171 + if h.isIgnored(name) { 172 + h.Write404(w) 171 173 return 172 174 } 173 175 treePath := chi.URLParam(r, "*") 174 176 ref := chi.URLParam(r, "ref") 175 177 176 178 name = filepath.Clean(name) 177 - path := filepath.Join(d.c.Repo.ScanPath, name) 179 + path := filepath.Join(h.c.Repo.ScanPath, name) 178 180 fmt.Println(path) 179 181 gr, err := git.Open(path, ref) 180 182 if err != nil { 181 - d.Write404(w) 183 + h.Write404(w) 182 184 return 183 185 } 184 186 185 187 files, err := gr.FileTree(treePath) 186 188 if err != nil { 187 - d.Write500(w) 189 + h.Write500(w) 188 190 log.Println(err) 189 191 return 190 192 } ··· 197 199 data["desc"] = getDescription(path) 198 200 data["dotdot"] = filepath.Dir(treePath) 199 201 200 - d.listFiles(files, data, w) 202 + h.listFiles(files, data, w) 201 203 return 202 204 } 203 205 204 - func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) { 206 + func (h *Handle) FileContent(w http.ResponseWriter, r *http.Request) { 205 207 var raw bool 206 208 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil { 207 209 raw = rawParam ··· 209 211 210 212 name := uniqueName(r) 211 213 212 - if d.isIgnored(name) { 213 - d.Write404(w) 214 + if h.isIgnored(name) { 215 + h.Write404(w) 214 216 return 215 217 } 216 218 treePath := chi.URLParam(r, "*") 217 219 ref := chi.URLParam(r, "ref") 218 220 219 221 name = filepath.Clean(name) 220 - path := filepath.Join(d.c.Repo.ScanPath, name) 222 + path := filepath.Join(h.c.Repo.ScanPath, name) 221 223 gr, err := git.Open(path, ref) 222 224 if err != nil { 223 - d.Write404(w) 225 + h.Write404(w) 224 226 return 225 227 } 226 228 227 229 contents, err := gr.FileContent(treePath) 228 230 if err != nil { 229 - d.Write500(w) 231 + h.Write500(w) 230 232 return 231 233 } 232 234 data := make(map[string]any) ··· 239 241 safe := sanitize([]byte(contents)) 240 242 241 243 if raw { 242 - d.showRaw(string(safe), w) 244 + h.showRaw(string(safe), w) 243 245 } else { 244 - if d.c.Meta.SyntaxHighlight == "" { 245 - d.showFile(string(safe), data, w) 246 + if h.c.Meta.SyntaxHighlight == "" { 247 + h.showFile(string(safe), data, w) 246 248 } else { 247 - d.showFileWithHighlight(treePath, string(safe), data, w) 249 + h.showFileWithHighlight(treePath, string(safe), data, w) 248 250 } 249 251 } 250 252 } 251 253 252 - func (d *deps) Archive(w http.ResponseWriter, r *http.Request) { 254 + func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) { 253 255 name := uniqueName(r) 254 - if d.isIgnored(name) { 255 - d.Write404(w) 256 + if h.isIgnored(name) { 257 + h.Write404(w) 256 258 return 257 259 } 258 260 ··· 260 262 261 263 // TODO: extend this to add more files compression (e.g.: xz) 262 264 if !strings.HasSuffix(file, ".tar.gz") { 263 - d.Write404(w) 265 + h.Write404(w) 264 266 return 265 267 } 266 268 ··· 272 274 setContentDisposition(w, filename) 273 275 setGZipMIME(w) 274 276 275 - path := filepath.Join(d.c.Repo.ScanPath, name) 277 + path := filepath.Join(h.c.Repo.ScanPath, name) 276 278 gr, err := git.Open(path, ref) 277 279 if err != nil { 278 - d.Write404(w) 280 + h.Write404(w) 279 281 return 280 282 } 281 283 ··· 300 302 } 301 303 } 302 304 303 - func (d *deps) Log(w http.ResponseWriter, r *http.Request) { 305 + func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 304 306 name := uniqueName(r) 305 - if d.isIgnored(name) { 306 - d.Write404(w) 307 + if h.isIgnored(name) { 308 + h.Write404(w) 307 309 return 308 310 } 309 311 ref := chi.URLParam(r, "ref") 310 312 311 - path := filepath.Join(d.c.Repo.ScanPath, name) 313 + path := filepath.Join(h.c.Repo.ScanPath, name) 312 314 gr, err := git.Open(path, ref) 313 315 if err != nil { 314 - d.Write404(w) 316 + h.Write404(w) 315 317 return 316 318 } 317 319 318 320 commits, err := gr.Commits() 319 321 if err != nil { 320 - d.Write500(w) 322 + h.Write500(w) 321 323 log.Println(err) 322 324 return 323 325 } 324 326 325 327 data := make(map[string]interface{}) 326 328 data["commits"] = commits 327 - data["meta"] = d.c.Meta 329 + data["meta"] = h.c.Meta 328 330 data["name"] = name 329 331 data["displayname"] = getDisplayName(name) 330 332 data["ref"] = ref 331 333 data["desc"] = getDescription(path) 332 334 data["log"] = true 333 335 334 - if err := d.t.ExecuteTemplate(w, "log", data); err != nil { 336 + if err := h.t.ExecuteTemplate(w, "log", data); err != nil { 335 337 log.Println(err) 336 338 return 337 339 } 338 340 } 339 341 340 - func (d *deps) Diff(w http.ResponseWriter, r *http.Request) { 342 + func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) { 341 343 name := uniqueName(r) 342 - if d.isIgnored(name) { 343 - d.Write404(w) 344 + if h.isIgnored(name) { 345 + h.Write404(w) 344 346 return 345 347 } 346 348 ref := chi.URLParam(r, "ref") 347 349 348 - path := filepath.Join(d.c.Repo.ScanPath, name) 350 + path := filepath.Join(h.c.Repo.ScanPath, name) 349 351 gr, err := git.Open(path, ref) 350 352 if err != nil { 351 - d.Write404(w) 353 + h.Write404(w) 352 354 return 353 355 } 354 356 355 357 diff, err := gr.Diff() 356 358 if err != nil { 357 - d.Write500(w) 359 + h.Write500(w) 358 360 log.Println(err) 359 361 return 360 362 } ··· 364 366 data["commit"] = diff.Commit 365 367 data["stat"] = diff.Stat 366 368 data["diff"] = diff.Diff 367 - data["meta"] = d.c.Meta 369 + data["meta"] = h.c.Meta 368 370 data["name"] = name 369 371 data["displayname"] = getDisplayName(name) 370 372 data["ref"] = ref 371 373 data["desc"] = getDescription(path) 372 374 373 - if err := d.t.ExecuteTemplate(w, "commit", data); err != nil { 375 + if err := h.t.ExecuteTemplate(w, "commit", data); err != nil { 374 376 log.Println(err) 375 377 return 376 378 } 377 379 } 378 380 379 - func (d *deps) Refs(w http.ResponseWriter, r *http.Request) { 381 + func (h *Handle) Refs(w http.ResponseWriter, r *http.Request) { 380 382 name := chi.URLParam(r, "name") 381 - if d.isIgnored(name) { 382 - d.Write404(w) 383 + if h.isIgnored(name) { 384 + h.Write404(w) 383 385 return 384 386 } 385 387 386 - path := filepath.Join(d.c.Repo.ScanPath, name) 388 + path := filepath.Join(h.c.Repo.ScanPath, name) 387 389 gr, err := git.Open(path, "") 388 390 if err != nil { 389 - d.Write404(w) 391 + h.Write404(w) 390 392 return 391 393 } 392 394 ··· 399 401 branches, err := gr.Branches() 400 402 if err != nil { 401 403 log.Println(err) 402 - d.Write500(w) 404 + h.Write500(w) 403 405 return 404 406 } 405 407 406 408 data := make(map[string]interface{}) 407 409 408 - data["meta"] = d.c.Meta 410 + data["meta"] = h.c.Meta 409 411 data["name"] = name 410 412 data["displayname"] = getDisplayName(name) 411 413 data["branches"] = branches 412 414 data["tags"] = tags 413 415 data["desc"] = getDescription(path) 414 416 415 - if err := d.t.ExecuteTemplate(w, "refs", data); err != nil { 417 + if err := h.t.ExecuteTemplate(w, "refs", data); err != nil { 416 418 log.Println(err) 417 419 return 418 420 } 419 421 } 420 422 421 - func (d *deps) ServeStatic(w http.ResponseWriter, r *http.Request) { 423 + func (h *Handle) ServeStatic(w http.ResponseWriter, r *http.Request) { 422 424 f := chi.URLParam(r, "file") 423 - f = filepath.Clean(filepath.Join(d.c.Dirs.Static, f)) 425 + f = filepath.Clean(filepath.Join(h.c.Dirs.Static, f)) 424 426 425 427 http.ServeFile(w, r, f) 426 428 } 427 429 428 - func (d *deps) Login(w http.ResponseWriter, r *http.Request) { 429 - if err := d.t.ExecuteTemplate(w, "login", nil); err != nil { 430 + func (h *Handle) Login(w http.ResponseWriter, r *http.Request) { 431 + if err := h.t.ExecuteTemplate(w, "login", nil); err != nil { 430 432 log.Println(err) 431 433 return 432 434 } 433 435 } 436 + 437 + func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 438 + switch r.Method { 439 + case http.MethodGet: 440 + // TODO: fetch keys from db 441 + if err := h.t.ExecuteTemplate(w, "keys", nil); err != nil { 442 + log.Println(err) 443 + return 444 + } 445 + case http.MethodPut: 446 + key := r.FormValue("key") 447 + name := r.FormValue("name") 448 + // TODO: add did here 449 + if err := h.db.AddPublicKey("did:ashtntnashtx", name, key); err != nil { 450 + h.WriteOOBNotice(w, "keys", "Failed to add key") 451 + log.Printf("adding public key: %s", err) 452 + return 453 + } 454 + 455 + h.WriteOOBNotice(w, "keys", "Key added!") 456 + return 457 + } 458 + }
+19 -14
legit/routes/template.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 + "fmt" 5 6 "html/template" 6 7 "io" 7 8 "log" ··· 15 16 "github.com/icyphox/bild/legit/git" 16 17 ) 17 18 18 - func (d *deps) Write404(w http.ResponseWriter) { 19 - tpath := filepath.Join(d.c.Dirs.Templates, "*") 20 - t := template.Must(template.ParseGlob(tpath)) 19 + func (h *Handle) Write404(w http.ResponseWriter) { 21 20 w.WriteHeader(404) 22 - if err := t.ExecuteTemplate(w, "404", nil); err != nil { 21 + if err := h.t.ExecuteTemplate(w, "404", nil); err != nil { 23 22 log.Printf("404 template: %s", err) 24 23 } 25 24 } 26 25 27 - func (d *deps) Write500(w http.ResponseWriter) { 28 - tpath := filepath.Join(d.c.Dirs.Templates, "*") 29 - t := template.Must(template.ParseGlob(tpath)) 26 + func (h *Handle) Write500(w http.ResponseWriter) { 30 27 w.WriteHeader(500) 31 - if err := t.ExecuteTemplate(w, "500", nil); err != nil { 28 + if err := h.t.ExecuteTemplate(w, "500", nil); err != nil { 32 29 log.Printf("500 template: %s", err) 33 30 } 34 31 } 35 32 36 - func (d *deps) listFiles(files []git.NiceTree, data map[string]any, w http.ResponseWriter) { 37 - tpath := filepath.Join(d.c.Dirs.Templates, "*") 33 + func (h *Handle) WriteOOBNotice(w http.ResponseWriter, id, msg string) { 34 + html := fmt.Sprintf(`<span id="%s" hx-swap-oob="innerHTML">%s</span>`, id, msg) 35 + 36 + w.Header().Set("Content-Type", "text/html") 37 + w.WriteHeader(http.StatusOK) 38 + w.Write([]byte(html)) 39 + } 40 + 41 + func (h *Handle) listFiles(files []git.NiceTree, data map[string]any, w http.ResponseWriter) { 42 + tpath := filepath.Join(h.c.Dirs.Templates, "*") 38 43 t := template.Must(template.ParseGlob(tpath)) 39 44 40 45 data["files"] = files 41 - data["meta"] = d.c.Meta 46 + data["meta"] = h.c.Meta 42 47 43 48 if err := t.ExecuteTemplate(w, "tree", data); err != nil { 44 49 log.Println(err) ··· 72 77 } 73 78 } 74 79 75 - func (d *deps) showFileWithHighlight(name, content string, data map[string]any, w http.ResponseWriter) { 80 + func (d *Handle) showFileWithHighlight(name, content string, data map[string]any, w http.ResponseWriter) { 76 81 tpath := filepath.Join(d.c.Dirs.Templates, "*") 77 82 t := template.Must(template.ParseGlob(tpath)) 78 83 ··· 114 119 } 115 120 } 116 121 117 - func (d *deps) showFile(content string, data map[string]any, w http.ResponseWriter) { 122 + func (d *Handle) showFile(content string, data map[string]any, w http.ResponseWriter) { 118 123 tpath := filepath.Join(d.c.Dirs.Templates, "*") 119 124 t := template.Must(template.ParseGlob(tpath)) 120 125 ··· 142 147 } 143 148 } 144 149 145 - func (d *deps) showRaw(content string, w http.ResponseWriter) { 150 + func (d *Handle) showRaw(content string, w http.ResponseWriter) { 146 151 w.WriteHeader(http.StatusOK) 147 152 w.Header().Set("Content-Type", "text/plain") 148 153 w.Write([]byte(content))
+6 -6
legit/routes/util.go
··· 43 43 return 44 44 } 45 45 46 - func (d *deps) isUnlisted(name string) bool { 47 - for _, i := range d.c.Repo.Unlisted { 46 + func (h *Handle) isUnlisted(name string) bool { 47 + for _, i := range h.c.Repo.Unlisted { 48 48 if name == i { 49 49 return true 50 50 } ··· 53 53 return false 54 54 } 55 55 56 - func (d *deps) isIgnored(name string) bool { 57 - for _, i := range d.c.Repo.Ignore { 56 + func (h *Handle) isIgnored(name string) bool { 57 + for _, i := range h.c.Repo.Ignore { 58 58 if name == i { 59 59 return true 60 60 } ··· 69 69 Category string 70 70 } 71 71 72 - func (d *deps) getAllRepos() ([]repoInfo, error) { 72 + func (d *Handle) getAllRepos() ([]repoInfo, error) { 73 73 repos := []repoInfo{} 74 74 max := strings.Count(d.c.Repo.ScanPath, string(os.PathSeparator)) + 2 75 75 ··· 113 113 return repos, err 114 114 } 115 115 116 - func (d *deps) category(path string) string { 116 + func (d *Handle) category(path string) string { 117 117 return strings.TrimPrefix(filepath.Dir(strings.TrimPrefix(path, d.c.Repo.ScanPath)), string(os.PathSeparator)) 118 118 } 119 119
+177 -167
legit/static/style.css
··· 1 1 :root { 2 - --white: #fff; 3 - --light: #f4f4f4; 4 - --cyan: #509c93; 5 - --light-gray: #eee; 6 - --medium-gray: #ddd; 7 - --gray: #6a6a6a; 8 - --dark: #444; 9 - --darker: #222; 2 + --white: #fff; 3 + --light: #f4f4f4; 4 + --cyan: #509c93; 5 + --light-gray: #eee; 6 + --medium-gray: #ddd; 7 + --gray: #6a6a6a; 8 + --dark: #444; 9 + --darker: #222; 10 10 11 - --sans-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Segoe UI", sans-serif; 12 - --display-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Segoe UI", sans-serif; 13 - --mono-font: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', 'Roboto Mono', Menlo, Consolas, monospace; 11 + --sans-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", 12 + "Segoe UI", sans-serif; 13 + --display-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", 14 + "Segoe UI", sans-serif; 15 + --mono-font: "SF Mono", SFMono-Regular, ui-monospace, "DejaVu Sans Mono", 16 + "Roboto Mono", Menlo, Consolas, monospace; 14 17 } 15 18 16 19 @media (prefers-color-scheme: dark) { 17 - :root { 18 - color-scheme: dark light; 19 - --light: #181818; 20 - --cyan: #76c7c0; 21 - --light-gray: #333; 22 - --medium-gray: #444; 23 - --gray: #aaa; 24 - --dark: #ddd; 25 - --darker: #f4f4f4; 26 - } 20 + :root { 21 + color-scheme: dark light; 22 + --white: #000; 23 + --light: #181818; 24 + --cyan: #76c7c0; 25 + --light-gray: #333; 26 + --medium-gray: #444; 27 + --gray: #aaa; 28 + --dark: #ddd; 29 + --darker: #f4f4f4; 30 + } 27 31 } 28 32 29 33 html { 30 - background: var(--white); 31 - -webkit-text-size-adjust: none; 32 - font-family: var(--sans-font); 33 - font-weight: 380; 34 + background: var(--white); 35 + -webkit-text-size-adjust: none; 36 + font-family: var(--sans-font); 37 + font-weight: 380; 34 38 } 35 39 36 40 pre { 37 - font-family: var(--mono-font); 41 + font-family: var(--mono-font); 38 42 } 39 43 40 44 ::selection { 41 - background: var(--medium-gray); 42 - opacity: 0.3; 45 + background: var(--medium-gray); 46 + opacity: 0.3; 43 47 } 44 48 45 49 * { 46 - box-sizing: border-box; 47 - padding: 0; 48 - margin: 0; 50 + box-sizing: border-box; 51 + padding: 0; 52 + margin: 0; 49 53 } 50 54 51 55 body { 52 - max-width: 1000px; 53 - padding: 0 13px; 54 - margin: 40px auto; 56 + max-width: 1000px; 57 + padding: 0 13px; 58 + margin: 40px auto; 55 59 } 56 60 57 - main, footer { 58 - font-size: 1rem; 59 - padding: 0; 60 - line-height: 160%; 61 + main, 62 + footer { 63 + font-size: 1rem; 64 + padding: 0; 65 + line-height: 160%; 61 66 } 62 67 63 - header h1, h2, h3 { 64 - font-family: var(--display-font); 68 + header h1, 69 + h2, 70 + h3 { 71 + font-family: var(--display-font); 65 72 } 66 73 67 74 h2 { 68 - font-weight: 400; 75 + font-weight: 400; 69 76 } 70 77 71 78 strong { 72 - font-weight: 500; 79 + font-weight: 500; 73 80 } 74 81 75 82 main h1 { 76 - padding: 10px 0 10px 0; 83 + padding: 10px 0 10px 0; 77 84 } 78 85 79 86 main h2 { 80 - font-size: 18px; 87 + font-size: 18px; 81 88 } 82 89 83 - main h2, h3 { 84 - padding: 20px 0 15px 0; 90 + main h2, 91 + h3 { 92 + padding: 20px 0 15px 0; 85 93 } 86 94 87 95 nav { 88 - padding: 0.4rem 0 1.5rem 0; 96 + padding: 0.4rem 0 1.5rem 0; 89 97 } 90 98 91 99 nav ul { 92 - padding: 0; 93 - margin: 0; 94 - list-style: none; 95 - padding-bottom: 20px; 100 + padding: 0; 101 + margin: 0; 102 + list-style: none; 103 + padding-bottom: 20px; 96 104 } 97 105 98 106 nav ul li { 99 - padding-right: 10px; 100 - display: inline-block; 107 + padding-right: 10px; 108 + display: inline-block; 101 109 } 102 110 103 111 a { 104 - margin: 0; 105 - padding: 0; 106 - box-sizing: border-box; 107 - text-decoration: none; 108 - word-wrap: break-word; 112 + margin: 0; 113 + padding: 0; 114 + box-sizing: border-box; 115 + text-decoration: none; 116 + word-wrap: break-word; 109 117 } 110 118 111 119 a { 112 - color: var(--darker); 113 - border-bottom: 1.5px solid var(--medium-gray); 120 + color: var(--darker); 121 + border-bottom: 1.5px solid var(--medium-gray); 114 122 } 115 123 116 124 a:hover { 117 - border-bottom: 1.5px solid var(--gray); 125 + border-bottom: 1.5px solid var(--gray); 118 126 } 119 127 120 128 .index { 121 - padding-top: 2em; 122 - display: grid; 123 - grid-template-columns: 6em 1fr minmax(0, 7em); 124 - grid-row-gap: 0.5em; 125 - min-width: 0; 129 + padding-top: 2em; 130 + display: grid; 131 + grid-template-columns: 6em 1fr minmax(0, 7em); 132 + grid-row-gap: 0.5em; 133 + min-width: 0; 126 134 } 127 135 128 136 .clone-url { 129 - padding-top: 2rem; 137 + padding-top: 2rem; 130 138 } 131 139 132 140 .clone-url pre { 133 - color: var(--dark); 134 - white-space: pre-wrap; 141 + color: var(--dark); 142 + white-space: pre-wrap; 135 143 } 136 144 137 145 .desc { 138 - font-weight: normal; 139 - color: var(--gray); 140 - font-style: italic; 146 + font-weight: normal; 147 + color: var(--gray); 148 + font-style: italic; 141 149 } 142 150 143 151 .tree { 144 - display: grid; 145 - grid-template-columns: 10ch auto 1fr; 146 - grid-row-gap: 0.5em; 147 - grid-column-gap: 1em; 148 - min-width: 0; 152 + display: grid; 153 + grid-template-columns: 10ch auto 1fr; 154 + grid-row-gap: 0.5em; 155 + grid-column-gap: 1em; 156 + min-width: 0; 149 157 } 150 158 151 159 .log { 152 - display: grid; 153 - grid-template-columns: 20rem minmax(0, 1fr); 154 - grid-row-gap: 0.8em; 155 - grid-column-gap: 8rem; 156 - margin-bottom: 2em; 157 - padding-bottom: 1em; 158 - border-bottom: 1.5px solid var(--medium-gray); 160 + display: grid; 161 + grid-template-columns: 20rem minmax(0, 1fr); 162 + grid-row-gap: 0.8em; 163 + grid-column-gap: 8rem; 164 + margin-bottom: 2em; 165 + padding-bottom: 1em; 166 + border-bottom: 1.5px solid var(--medium-gray); 159 167 } 160 168 161 169 .log pre { 162 - white-space: pre-wrap; 170 + white-space: pre-wrap; 163 171 } 164 172 165 - .mode, .size { 166 - font-family: var(--mono-font); 173 + .mode, 174 + .size { 175 + font-family: var(--mono-font); 167 176 } 168 177 .size { 169 - text-align: right; 178 + text-align: right; 170 179 } 171 180 172 181 .readme pre { 173 - white-space: pre-wrap; 174 - overflow-x: auto; 182 + white-space: pre-wrap; 183 + overflow-x: auto; 175 184 } 176 185 177 186 .readme { 178 - background: var(--light-gray); 179 - padding: 0.5rem; 187 + background: var(--light-gray); 188 + padding: 0.5rem; 180 189 } 181 190 182 191 .readme ul { 183 - padding: revert; 192 + padding: revert; 184 193 } 185 194 186 195 .readme img { 187 - max-width: 100%; 196 + max-width: 100%; 188 197 } 189 198 190 199 .diff { 191 - margin: 1rem 0 1rem 0; 192 - padding: 1rem 0 1rem 0; 193 - border-bottom: 1.5px solid var(--medium-gray); 200 + margin: 1rem 0 1rem 0; 201 + padding: 1rem 0 1rem 0; 202 + border-bottom: 1.5px solid var(--medium-gray); 194 203 } 195 204 196 205 .diff pre { 197 - overflow: scroll; 206 + overflow: scroll; 198 207 } 199 208 200 209 .diff-stat { 201 - padding: 1rem 0 1rem 0; 210 + padding: 1rem 0 1rem 0; 202 211 } 203 212 204 - .commit-hash, .commit-email { 205 - font-family: var(--mono-font); 213 + .commit-hash, 214 + .commit-email { 215 + font-family: var(--mono-font); 206 216 } 207 217 208 218 .commit-email:before { 209 - content: '<'; 219 + content: "<"; 210 220 } 211 221 212 222 .commit-email:after { 213 - content: '>'; 223 + content: ">"; 214 224 } 215 225 216 226 .commit { 217 - margin-bottom: 1rem; 227 + margin-bottom: 1rem; 218 228 } 219 229 220 230 .commit pre { 221 - padding-bottom: 1rem; 222 - white-space: pre-wrap; 231 + padding-bottom: 1rem; 232 + white-space: pre-wrap; 223 233 } 224 234 225 235 .diff-stat ul li { 226 - list-style: none; 227 - padding-left: 0.5em; 236 + list-style: none; 237 + padding-left: 0.5em; 228 238 } 229 239 230 240 .diff-add { 231 - color: green; 241 + color: green; 232 242 } 233 243 234 244 .diff-del { 235 - color: red; 245 + color: red; 236 246 } 237 247 238 248 .diff-noop { 239 - color: var(--gray); 249 + color: var(--gray); 240 250 } 241 251 242 252 .ref { 243 - font-family: var(--sans-font); 244 - font-size: 14px; 245 - color: var(--gray); 246 - display: inline-block; 247 - padding-top: 0.7em; 253 + font-family: var(--sans-font); 254 + font-size: 14px; 255 + color: var(--gray); 256 + display: inline-block; 257 + padding-top: 0.7em; 248 258 } 249 259 250 260 .refs pre { 251 - white-space: pre-wrap; 252 - padding-bottom: 0.5rem; 261 + white-space: pre-wrap; 262 + padding-bottom: 0.5rem; 253 263 } 254 264 255 265 .refs strong { 256 - padding-right: 1em; 266 + padding-right: 1em; 257 267 } 258 268 259 269 .line-numbers { 260 - white-space: pre-line; 261 - -moz-user-select: -moz-none; 262 - -khtml-user-select: none; 263 - -webkit-user-select: none; 264 - -o-user-select: none; 265 - user-select: none; 266 - display: flex; 267 - float: left; 268 - flex-direction: column; 269 - margin-right: 1ch; 270 + white-space: pre-line; 271 + -moz-user-select: -moz-none; 272 + -khtml-user-select: none; 273 + -webkit-user-select: none; 274 + -o-user-select: none; 275 + user-select: none; 276 + display: flex; 277 + float: left; 278 + flex-direction: column; 279 + margin-right: 1ch; 270 280 } 271 281 272 282 .file-wrapper { 273 - display: flex; 274 - flex-direction: row; 275 - grid-template-columns: 1rem minmax(0, 1fr); 276 - gap: 1rem; 277 - padding: 0.5rem; 278 - background: var(--light-gray); 279 - overflow-x: auto; 283 + display: flex; 284 + flex-direction: row; 285 + grid-template-columns: 1rem minmax(0, 1fr); 286 + gap: 1rem; 287 + padding: 0.5rem; 288 + background: var(--light-gray); 289 + overflow-x: auto; 280 290 } 281 291 282 292 .chroma-file-wrapper { 283 - display: flex; 284 - flex-direction: row; 285 - grid-template-columns: 1rem minmax(0, 1fr); 286 - overflow-x: auto; 293 + display: flex; 294 + flex-direction: row; 295 + grid-template-columns: 1rem minmax(0, 1fr); 296 + overflow-x: auto; 287 297 } 288 298 289 299 .file-content { 290 - background: var(--light-gray); 291 - overflow-y: hidden; 292 - overflow-x: auto; 300 + background: var(--light-gray); 301 + overflow-y: hidden; 302 + overflow-x: auto; 293 303 } 294 304 295 305 .diff-type { 296 - color: var(--gray); 306 + color: var(--gray); 297 307 } 298 308 299 309 .commit-info { 300 - color: var(--gray); 301 - padding-bottom: 1.5rem; 302 - font-size: 0.85rem; 310 + color: var(--gray); 311 + padding-bottom: 1.5rem; 312 + font-size: 0.85rem; 303 313 } 304 314 305 315 @media (max-width: 600px) { 306 - .index { 307 - grid-row-gap: 0.8em; 308 - } 316 + .index { 317 + grid-row-gap: 0.8em; 318 + } 309 319 310 - .log { 311 - grid-template-columns: 1fr; 312 - grid-row-gap: 0em; 313 - } 320 + .log { 321 + grid-template-columns: 1fr; 322 + grid-row-gap: 0em; 323 + } 314 324 315 - .index { 316 - grid-template-columns: 1fr; 317 - grid-row-gap: 0em; 318 - } 325 + .index { 326 + grid-template-columns: 1fr; 327 + grid-row-gap: 0em; 328 + } 319 329 320 - .index-name:not(:first-child) { 321 - padding-top: 1.5rem; 322 - } 330 + .index-name:not(:first-child) { 331 + padding-top: 1.5rem; 332 + } 323 333 324 - .commit-info:not(:last-child) { 325 - padding-bottom: 1.5rem; 326 - } 334 + .commit-info:not(:last-child) { 335 + padding-bottom: 1.5rem; 336 + } 327 337 328 - pre { 329 - font-size: 0.8rem; 330 - } 338 + pre { 339 + font-size: 0.8rem; 340 + } 331 341 }
+22 -15
legit/templates/head.html
··· 1 1 {{ define "head" }} 2 - <head> 3 - <meta charset="utf-8"> 4 - <meta name="viewport" content="width=device-width, initial-scale=1"> 5 - <link rel="stylesheet" href="/static/style.css" type="text/css"> 6 - <link rel="icon" type="image/png" size="32x32" href="/static/legit.png"> 2 + <head> 3 + <meta charset="utf-8" /> 4 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 5 + <link rel="stylesheet" href="/static/style.css" type="text/css" /> 6 + <link rel="icon" type="image/png" size="32x32" href="/static/legit.png" /> 7 + <script src="https://unpkg.com/htmx.org@2.0.4"></script> 8 + <meta name="htmx-config" content='{"selfRequestsOnly":false}' /> 9 + 7 10 {{ if .parent }} 8 - <title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .parent }}/</title> 11 + <title> 12 + {{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .parent }}/ 13 + </title> 9 14 10 15 {{ else if .path }} 11 - <title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .path }}</title> 16 + <title> 17 + {{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .path }} 18 + </title> 12 19 {{ else if .files }} 13 20 <title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }})</title> 14 21 {{ else if .commit }} 15 22 <title>{{ .meta.Title }} &mdash; {{ .name }}: {{ .commit.This }}</title> 16 23 {{ else if .branches }} 17 24 <title>{{ .meta.Title }} &mdash; {{ .name }}: refs</title> 18 - {{ else if .commits }} 19 - {{ if .log }} 25 + {{ else if .commits }} {{ if .log }} 20 26 <title>{{ .meta.Title }} &mdash; {{ .name }}: log</title> 21 27 {{ else }} 22 28 <title>{{ .meta.Title }} &mdash; {{ .name }}</title> 23 - {{ end }} 24 - {{ else }} 29 + {{ end }} {{ else }} 25 30 <title>{{ .meta.Title }}</title> 26 - {{ end }} 27 - {{ if and .servername .gomod }} 28 - <meta name="go-import" content="{{ .servername}}/{{ .name }} git https://{{ .servername }}/{{ .name }}"> 31 + {{ end }} {{ if and .servername .gomod }} 32 + <meta 33 + name="go-import" 34 + content="{{ .servername}}/{{ .name }} git https://{{ .servername }}/{{ .name }}" 35 + /> 29 36 {{ end }} 30 37 <!-- other meta tags here --> 31 - </head> 38 + </head> 32 39 {{ end }}
+35
legit/templates/keys.html
··· 1 + {{ define "keys" }} 2 + <html> 3 + {{ template "head" . }} 4 + 5 + <body> 6 + <main> 7 + <form> 8 + <p> 9 + Give your key a name and paste your 10 + <strong>public</strong> key here. This is what you'll use to 11 + push to your Git repository. 12 + </p> 13 + <div id="keys"></div> 14 + <div> 15 + <input 16 + type="text" 17 + id="name" 18 + name="name" 19 + placeholder="my laptop" 20 + /> 21 + <input 22 + type="text" 23 + id="public_key" 24 + name="key" 25 + placeholder="ssh-ed25519 AAABBBHUNTER2..." 26 + /> 27 + </div> 28 + <button hx-put="/settings/keys" hx-swap="none" type="submit"> 29 + Submit 30 + </button> 31 + </form> 32 + </main> 33 + </body> 34 + </html> 35 + {{ end }}
+2 -2
legit/templates/login.html
··· 10 10 action="http://localhost:3000/login" 11 11 > 12 12 <p> 13 - You will be redirected to bsky.app (or your PDS) to complete 14 - login. 13 + You will be redirected to bsky.social (or your PDS) to 14 + complete login. 15 15 </p> 16 16 <div> 17 17 <input