+8
-4
cmd/legit/main.go
+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
+1
config.yaml
+1
go.mod
+1
go.mod
+2
go.sum
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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 }} — {{ .name }} ({{ .ref }}): {{ .parent }}/</title>
11
+
<title>
12
+
{{ .meta.Title }} — {{ .name }} ({{ .ref }}): {{ .parent }}/
13
+
</title>
9
14
10
15
{{ else if .path }}
11
-
<title>{{ .meta.Title }} — {{ .name }} ({{ .ref }}): {{ .path }}</title>
16
+
<title>
17
+
{{ .meta.Title }} — {{ .name }} ({{ .ref }}): {{ .path }}
18
+
</title>
12
19
{{ else if .files }}
13
20
<title>{{ .meta.Title }} — {{ .name }} ({{ .ref }})</title>
14
21
{{ else if .commit }}
15
22
<title>{{ .meta.Title }} — {{ .name }}: {{ .commit.This }}</title>
16
23
{{ else if .branches }}
17
24
<title>{{ .meta.Title }} — {{ .name }}: refs</title>
18
-
{{ else if .commits }}
19
-
{{ if .log }}
25
+
{{ else if .commits }} {{ if .log }}
20
26
<title>{{ .meta.Title }} — {{ .name }}: log</title>
21
27
{{ else }}
22
28
<title>{{ .meta.Title }} — {{ .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
+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 }}