An experimental IndieWeb site built in Go.

Compare changes

Choose any two refs to compare.

+1
.gitignore
··· 4 4 5 5 data/**/*.db 6 6 tmp/ 7 + *_templ.txt 7 8 8 9 static/styles.css
+47
README.md
··· 1 + # Space 2 + 3 + > A likely-to-be-unfinished experimental IndieWeb site, built with Go. 4 + 5 + Space is an attempt at building an IndieWeb-capable personal site, before I 6 + started looking into AT Protocol. It uses [Chi](https://go-chi.io/#/), 7 + [Templ](https://templ.guide/), and [TailwindCSS](https://tailwindcss.com). 8 + Data is stored using SQLite, and your DB is continuously backed up to S3 via 9 + [Litestream](https://litestream.io/). 10 + 11 + Any unrecognized or unsupported [post types](https://indieweb.org/posts) are 12 + rendered as raw JSON until they are explicitly supported. Media files are 13 + stored in the same S3 backend that Litestream uses, so all your data is 14 + replicated off-machine. 15 + 16 + Notably, the Tailwind pipeline is not checked into the project, as this was 17 + built before Go Tool support. 18 + 19 + If you want to take it for a test-drive, you can check the `.env.example` file 20 + for all required configuration values. 21 + 22 + ## Supported Specs 23 + 24 + - [x] [IndieAuth](https://www.w3.org/TR/indieauth/) via Basic Auth 25 + - [x] [Micropub](https://www.w3.org/TR/micropub/) 26 + - [ ] [h-card](https://microformats.org/wiki/h-card) 27 + - [ ] [Webmentions](https://www.w3.org/TR/webmention/) 28 + - [ ] [Microsub](https://indieweb.org/Microsub-spec) 29 + 30 + ## What happened? 31 + 32 + At the time, I was quite pleased with how this turned out. I think I nailed the 33 + aesthetics, and I hit my requirement for data durability and replication. I was 34 + targeting Fly.io for deployment, and Fly+Tigris was an excellent combo. 35 + 36 + Day-to-day, though, I couldn't make a habit of using this. IndieWeb is a lonely 37 + place when you don't have any connections and you're not a social creature by 38 + default. Implementing support for different post types over time felt like it 39 + was going to be a drag, and my heart just wasn't in it. 40 + 41 + I have my hopes up with AT Protocol, however. AppViews remove the need for 42 + implementing display logic for each "post type" manually, and Lexicons allow 43 + for myriad post types contributed by different services. It's an easier place 44 + to live for a lurker such as myself. 45 + 46 + Maybe at some point, I'll take a stab at implementing my own PDS, but until 47 + then--here's the thing I already built.
+3 -3
config/Dockerfile
··· 1 1 # Build styles 2 - FROM denoland/deno:1.45.5 AS build-styles 2 + FROM denoland/deno:2.5.2 AS build-styles 3 3 WORKDIR /app 4 4 5 5 COPY . /app 6 6 7 - RUN deno run --allow-all npm:tailwindcss -i config/main.css -o static/styles.css -c config/tailwind.config.ts --minify 7 + RUN deno run --allow-all npm:tailwindcss@3.4.17 -i config/main.css -o static/styles.css -c config/tailwind.config.ts --minify 8 8 9 - FROM golang:1.22-alpine AS build-server 9 + FROM golang:1.24-alpine AS build-server 10 10 WORKDIR /app 11 11 12 12 COPY . /app
+2 -2
handlers/routes.go
··· 18 18 panic(result.Error) 19 19 } 20 20 21 - layouts.RenderDefault(pages.Home(posts)).ServeHTTP(w, r) 21 + layouts.RenderWithSidebar("", pages.Home(posts)).ServeHTTP(w, r) 22 22 } 23 23 24 24 func ServePostPage(w http.ResponseWriter, r *http.Request) { ··· 32 32 return 33 33 } 34 34 35 - layouts.RenderDefault(pages.Post(post)).ServeHTTP(w, r) 35 + layouts.RenderWithSidebar(string(post.MicroformatType), pages.Post(post)).ServeHTTP(w, r) 36 36 }
+21
html/components/head.templ
··· 1 + package components 2 + 3 + import "github.com/puregarlic/space/storage" 4 + 5 + templ Head(title string) { 6 + <head> 7 + if len(title) > 0 { 8 + <title>{ title } | puregarlic dot space</title> 9 + } else { 10 + <title>puregarlic dot space</title> 11 + } 12 + <meta name="viewport" content="width=device-width, initial-scale=1"/> 13 + <link rel="preconnect" href="https://fonts.googleapis.com"/> 14 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/> 15 + <link href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap" rel="stylesheet"/> 16 + for _, rel := range storage.GetRels() { 17 + <link rel={ rel.Name } href={ rel.HREF }/> 18 + } 19 + <link rel="stylesheet" href="/static/styles.css"/> 20 + </head> 21 + }
+101
html/components/head_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package components 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import "github.com/puregarlic/space/storage" 12 + 13 + func Head(title string) templ.Component { 14 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 15 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 16 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 17 + if !templ_7745c5c3_IsBuffer { 18 + defer func() { 19 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 20 + if templ_7745c5c3_Err == nil { 21 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 22 + } 23 + }() 24 + } 25 + ctx = templ.InitializeContext(ctx) 26 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 27 + if templ_7745c5c3_Var1 == nil { 28 + templ_7745c5c3_Var1 = templ.NopComponent 29 + } 30 + ctx = templ.ClearChildren(ctx) 31 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<head>") 32 + if templ_7745c5c3_Err != nil { 33 + return templ_7745c5c3_Err 34 + } 35 + if len(title) > 0 { 36 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<title>") 37 + if templ_7745c5c3_Err != nil { 38 + return templ_7745c5c3_Err 39 + } 40 + var templ_7745c5c3_Var2 string 41 + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) 42 + if templ_7745c5c3_Err != nil { 43 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/head.templ`, Line: 8, Col: 17} 44 + } 45 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 46 + if templ_7745c5c3_Err != nil { 47 + return templ_7745c5c3_Err 48 + } 49 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" | puregarlic dot space</title>") 50 + if templ_7745c5c3_Err != nil { 51 + return templ_7745c5c3_Err 52 + } 53 + } else { 54 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<title>puregarlic dot space</title>") 55 + if templ_7745c5c3_Err != nil { 56 + return templ_7745c5c3_Err 57 + } 58 + } 59 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin><link href=\"https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&amp;display=swap\" rel=\"stylesheet\">") 60 + if templ_7745c5c3_Err != nil { 61 + return templ_7745c5c3_Err 62 + } 63 + for _, rel := range storage.GetRels() { 64 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<link rel=\"") 65 + if templ_7745c5c3_Err != nil { 66 + return templ_7745c5c3_Err 67 + } 68 + var templ_7745c5c3_Var3 string 69 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(rel.Name) 70 + if templ_7745c5c3_Err != nil { 71 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/head.templ`, Line: 17, Col: 23} 72 + } 73 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 74 + if templ_7745c5c3_Err != nil { 75 + return templ_7745c5c3_Err 76 + } 77 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" href=\"") 78 + if templ_7745c5c3_Err != nil { 79 + return templ_7745c5c3_Err 80 + } 81 + var templ_7745c5c3_Var4 string 82 + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(rel.HREF) 83 + if templ_7745c5c3_Err != nil { 84 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/head.templ`, Line: 17, Col: 41} 85 + } 86 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 87 + if templ_7745c5c3_Err != nil { 88 + return templ_7745c5c3_Err 89 + } 90 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") 91 + if templ_7745c5c3_Err != nil { 92 + return templ_7745c5c3_Err 93 + } 94 + } 95 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<link rel=\"stylesheet\" href=\"/static/styles.css\"></head>") 96 + if templ_7745c5c3_Err != nil { 97 + return templ_7745c5c3_Err 98 + } 99 + return templ_7745c5c3_Err 100 + }) 101 + }
+4 -6
html/components/posts/note.templ
··· 1 1 package posts 2 2 3 - import ( 4 - "github.com/puregarlic/space/models" 5 - ) 3 + import "github.com/puregarlic/space/models" 6 4 7 5 templ Note(post *models.Post) { 8 - <div class="bg-surface p-4"> 9 - {GetPostJSONProperty(post, "content")[0]} 10 - </div> 6 + <div class="bg-surface p-4 first:rounded-t last:rounded-b border-2 border-overlay"> 7 + { GetPostJSONProperty(post, "content")[0] } 8 + </div> 11 9 }
+3 -5
html/components/posts/note_templ.go
··· 8 8 import "github.com/a-h/templ" 9 9 import templruntime "github.com/a-h/templ/runtime" 10 10 11 - import ( 12 - "github.com/puregarlic/space/models" 13 - ) 11 + import "github.com/puregarlic/space/models" 14 12 15 13 func Note(post *models.Post) templ.Component { 16 14 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { ··· 30 28 templ_7745c5c3_Var1 = templ.NopComponent 31 29 } 32 30 ctx = templ.ClearChildren(ctx) 33 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"bg-surface p-4\">") 31 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"bg-surface p-4 first:rounded-t last:rounded-b border-2 border-overlay\">") 34 32 if templ_7745c5c3_Err != nil { 35 33 return templ_7745c5c3_Err 36 34 } 37 35 var templ_7745c5c3_Var2 string 38 36 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(GetPostJSONProperty(post, "content")[0]) 39 37 if templ_7745c5c3_Err != nil { 40 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/note.templ`, Line: 9, Col: 43} 38 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/note.templ`, Line: 7, Col: 43} 41 39 } 42 40 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 43 41 if templ_7745c5c3_Err != nil {
+13 -15
html/components/posts/photo.templ
··· 1 1 package posts 2 2 3 - import ( 4 - "github.com/puregarlic/space/models" 5 - ) 3 + import "github.com/puregarlic/space/models" 6 4 7 5 templ Photo(post *models.Post) { 8 - <div class="bg-base"> 9 - for index, photo := range GetPostJSONProperty(post, "photo") { 10 - <figure class="relative group"> 11 - <img class="w-full" src={photo} /> 12 - <figcaption 13 - class="p-4 absolute inset-x-0 bottom-0 opacity-100 group-hover:opacity-0 bg-surface/70 backdrop-blur transition" 14 - > 15 - {GetPostJSONProperty(post, "content")[index]} 16 - </figcaption> 17 - </figure> 18 - } 19 - </div> 6 + <div class="bg-base"> 7 + for index, photo := range GetPostJSONProperty(post, "photo") { 8 + <figure class="relative group last:rounded-b"> 9 + <img class="w-full border-x-2 border-overlay group-first:rounded-t group-first:border-t-2" src={ photo }/> 10 + <figcaption 11 + class="p-4 bg-surface group-last:rounded-b group-last:border-b-2 group-last:border-x-2 group-last:border-overlay" 12 + > 13 + { GetPostJSONProperty(post, "content")[index] } 14 + </figcaption> 15 + </figure> 16 + } 17 + </div> 20 18 }
+5 -7
html/components/posts/photo_templ.go
··· 8 8 import "github.com/a-h/templ" 9 9 import templruntime "github.com/a-h/templ/runtime" 10 10 11 - import ( 12 - "github.com/puregarlic/space/models" 13 - ) 11 + import "github.com/puregarlic/space/models" 14 12 15 13 func Photo(post *models.Post) templ.Component { 16 14 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { ··· 35 33 return templ_7745c5c3_Err 36 34 } 37 35 for index, photo := range GetPostJSONProperty(post, "photo") { 38 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<figure class=\"relative group\"><img class=\"w-full\" src=\"") 36 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<figure class=\"relative group last:rounded-b\"><img class=\"w-full border-x-2 border-overlay group-first:rounded-t group-first:border-t-2\" src=\"") 39 37 if templ_7745c5c3_Err != nil { 40 38 return templ_7745c5c3_Err 41 39 } 42 40 var templ_7745c5c3_Var2 string 43 41 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(photo) 44 42 if templ_7745c5c3_Err != nil { 45 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/photo.templ`, Line: 11, Col: 35} 43 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/photo.templ`, Line: 9, Col: 106} 46 44 } 47 45 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 48 46 if templ_7745c5c3_Err != nil { 49 47 return templ_7745c5c3_Err 50 48 } 51 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><figcaption class=\"p-4 absolute inset-x-0 bottom-0 opacity-100 group-hover:opacity-0 bg-surface/70 backdrop-blur transition\">") 49 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><figcaption class=\"p-4 bg-surface group-last:rounded-b group-last:border-b-2 group-last:border-x-2 group-last:border-overlay\">") 52 50 if templ_7745c5c3_Err != nil { 53 51 return templ_7745c5c3_Err 54 52 } 55 53 var templ_7745c5c3_Var3 string 56 54 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(GetPostJSONProperty(post, "content")[index]) 57 55 if templ_7745c5c3_Err != nil { 58 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/photo.templ`, Line: 15, Col: 52} 56 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/photo.templ`, Line: 13, Col: 50} 59 57 } 60 58 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 61 59 if templ_7745c5c3_Err != nil {
+66 -50
html/components/posts/post.templ
··· 1 1 package posts 2 2 3 3 import ( 4 - "fmt" 5 4 "encoding/json" 6 - 7 - "github.com/puregarlic/space/models" 5 + "fmt" 8 6 9 - "github.com/samber/lo" 7 + "github.com/puregarlic/space/models" 8 + "github.com/samber/lo" 10 9 "go.hacdias.com/indielib/microformats" 11 10 ) 12 11 13 12 var ImplementedPostTypes = []microformats.Type{ 14 - microformats.TypeNote, 15 - microformats.TypePhoto, 13 + microformats.TypeNote, 14 + microformats.TypePhoto, 16 15 } 17 16 18 17 func GetPostJSONProperty(post *models.Post, name string) []string { 19 - var tmp map[string]any 20 - if err := json.Unmarshal(post.Properties, &tmp); err != nil { 21 - panic(err) 22 - } 18 + var tmp map[string]any 19 + if err := json.Unmarshal(post.Properties, &tmp); err != nil { 20 + panic(err) 21 + } 23 22 24 - prop, ok := tmp[name] 25 - if !ok { 26 - return []string{""} 27 - } 23 + prop, ok := tmp[name] 24 + if !ok { 25 + return []string{""} 26 + } 28 27 29 - var out []string 30 - for _, val := range prop.([]any) { 31 - out = append(out, val.(string)) 32 - } 28 + var out []string 29 + for _, val := range prop.([]any) { 30 + out = append(out, val.(string)) 31 + } 33 32 34 - return out 33 + return out 35 34 } 36 35 37 36 func formatPostTypeName(mfType microformats.Type) string { 38 - has := lo.ContainsBy(ImplementedPostTypes, func(postType microformats.Type) bool { 39 - return postType == mfType 40 - }) 37 + has := lo.ContainsBy(ImplementedPostTypes, func(postType microformats.Type) bool { 38 + return postType == mfType 39 + }) 41 40 42 - if has { 43 - return string(mfType) 44 - } else { 45 - return fmt.Sprintf("oops! (%s)", string(mfType)) 46 - } 41 + if has { 42 + return string(mfType) 43 + } else { 44 + return fmt.Sprintf("%s (oops!)", string(mfType)) 45 + } 47 46 } 48 47 49 48 templ PostFeedHeader(post *models.Post) { 50 - <div class="px-3 py-2 bg-base text-xs text-subtle/50 group-hover:text-subtle/100 flex justify-between transition"> 51 - <p>{ formatPostTypeName(post.MicroformatType) }</p> 52 - <a 53 - class="hover:underline hover:text-iris flex gap-1" 54 - target="_blank" 55 - rel="noopener noreferrer" 56 - href={ templ.URL("/posts/" + post.ID.String()) } 57 - > 58 - { post.CreatedAt.Format("01/02/2006 at 3:04 PM") } 59 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4 -mt-px"> 60 - <path d="M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z" /> 61 - <path d="M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z" /> 62 - </svg> 63 - </a> 64 - </div> 49 + <div class="px-3 py-2 bg-surface text-xs text-muted flex items-center justify-between rounded-t border-2 border-b-0 border-overlay"> 50 + <p>{ post.Timestamp() }</p> 51 + <p class="flex gap-1.5"> 52 + { formatPostTypeName(post.MicroformatType) } 53 + <span class="text-muted/40">&#8226;</span> 54 + <a 55 + class="hover:underline hover:text-iris flex items-center gap-1 transition" 56 + target="_blank" 57 + rel="noopener noreferrer" 58 + href={ templ.URL("/posts/" + post.ID.String()) } 59 + > 60 + open 61 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4 -mt-px"> 62 + <path d="M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z"></path> 63 + <path d="M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z"></path> 64 + </svg> 65 + </a> 66 + </p> 67 + </div> 68 + } 69 + 70 + templ PostDetails(post *models.Post) { 71 + <dl class="grid md:grid-cols-2 gap-4"> 72 + <div> 73 + <dt class="mb-1 text-sm text-muted">Posted At</dt> 74 + <dd class="text-subtle">{ post.Timestamp() }</dd> 75 + </div> 76 + <div> 77 + <dt class="mb-1 text-sm text-muted">Post Type</dt> 78 + <dd class="text-subtle">{ string(post.MicroformatType) }</dd> 79 + </div> 80 + </dl> 65 81 } 66 82 67 83 templ PostContent(post *models.Post) { 68 - switch post.MicroformatType { 69 - case microformats.TypePhoto: 70 - @Photo(post) 71 - case microformats.TypeNote: 72 - @Note(post) 73 - default: 74 - @Unsupported(post) 75 - } 84 + switch post.MicroformatType { 85 + case microformats.TypePhoto: 86 + @Photo(post) 87 + case microformats.TypeNote: 88 + @Note(post) 89 + default: 90 + @Unsupported(post) 91 + } 76 92 }
+67 -16
html/components/posts/post_templ.go
··· 13 13 "fmt" 14 14 15 15 "github.com/puregarlic/space/models" 16 - 17 16 "github.com/samber/lo" 18 17 "go.hacdias.com/indielib/microformats" 19 18 ) ··· 50 49 if has { 51 50 return string(mfType) 52 51 } else { 53 - return fmt.Sprintf("oops! (%s)", string(mfType)) 52 + return fmt.Sprintf("%s (oops!)", string(mfType)) 54 53 } 55 54 } 56 55 ··· 72 71 templ_7745c5c3_Var1 = templ.NopComponent 73 72 } 74 73 ctx = templ.ClearChildren(ctx) 75 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"px-3 py-2 bg-base text-xs text-subtle/50 group-hover:text-subtle/100 flex justify-between transition\"><p>") 74 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"px-3 py-2 bg-surface text-xs text-muted flex items-center justify-between rounded-t border-2 border-b-0 border-overlay\"><p>") 76 75 if templ_7745c5c3_Err != nil { 77 76 return templ_7745c5c3_Err 78 77 } 79 78 var templ_7745c5c3_Var2 string 80 - templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(formatPostTypeName(post.MicroformatType)) 79 + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(post.Timestamp()) 81 80 if templ_7745c5c3_Err != nil { 82 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 51, Col: 49} 81 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 50, Col: 23} 83 82 } 84 83 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 85 84 if templ_7745c5c3_Err != nil { 86 85 return templ_7745c5c3_Err 87 86 } 88 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><a class=\"hover:underline hover:text-iris flex gap-1\" target=\"_blank\" rel=\"noopener noreferrer\" href=\"") 87 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"flex gap-1.5\">") 89 88 if templ_7745c5c3_Err != nil { 90 89 return templ_7745c5c3_Err 91 90 } 92 - var templ_7745c5c3_Var3 templ.SafeURL = templ.URL("/posts/" + post.ID.String()) 93 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3))) 91 + var templ_7745c5c3_Var3 string 92 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(formatPostTypeName(post.MicroformatType)) 94 93 if templ_7745c5c3_Err != nil { 95 - return templ_7745c5c3_Err 94 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 52, Col: 45} 96 95 } 97 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") 96 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 98 97 if templ_7745c5c3_Err != nil { 99 98 return templ_7745c5c3_Err 100 99 } 101 - var templ_7745c5c3_Var4 string 102 - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(post.CreatedAt.Format("01/02/2006 at 3:04 PM")) 100 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <span class=\"text-muted/40\">&#8226;</span> <a class=\"hover:underline hover:text-iris flex items-center gap-1 transition\" target=\"_blank\" rel=\"noopener noreferrer\" href=\"") 103 101 if templ_7745c5c3_Err != nil { 104 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 58, Col: 53} 102 + return templ_7745c5c3_Err 105 103 } 106 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 104 + var templ_7745c5c3_Var4 templ.SafeURL = templ.URL("/posts/" + post.ID.String()) 105 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4))) 107 106 if templ_7745c5c3_Err != nil { 108 107 return templ_7745c5c3_Err 109 108 } 110 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"size-4 -mt-px\"><path d=\"M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z\"></path> <path d=\"M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z\"></path></svg></a></div>") 109 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">open <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"size-4 -mt-px\"><path d=\"M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z\"></path> <path d=\"M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z\"></path></svg></a></p></div>") 111 110 if templ_7745c5c3_Err != nil { 112 111 return templ_7745c5c3_Err 113 112 } ··· 115 114 }) 116 115 } 117 116 118 - func PostContent(post *models.Post) templ.Component { 117 + func PostDetails(post *models.Post) templ.Component { 119 118 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 120 119 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 121 120 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) ··· 131 130 templ_7745c5c3_Var5 := templ.GetChildren(ctx) 132 131 if templ_7745c5c3_Var5 == nil { 133 132 templ_7745c5c3_Var5 = templ.NopComponent 133 + } 134 + ctx = templ.ClearChildren(ctx) 135 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<dl class=\"grid md:grid-cols-2 gap-4\"><div><dt class=\"mb-1 text-sm text-muted\">Posted At</dt><dd class=\"text-subtle\">") 136 + if templ_7745c5c3_Err != nil { 137 + return templ_7745c5c3_Err 138 + } 139 + var templ_7745c5c3_Var6 string 140 + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(post.Timestamp()) 141 + if templ_7745c5c3_Err != nil { 142 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 74, Col: 45} 143 + } 144 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 145 + if templ_7745c5c3_Err != nil { 146 + return templ_7745c5c3_Err 147 + } 148 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</dd></div><div><dt class=\"mb-1 text-sm text-muted\">Post Type</dt><dd class=\"text-subtle\">") 149 + if templ_7745c5c3_Err != nil { 150 + return templ_7745c5c3_Err 151 + } 152 + var templ_7745c5c3_Var7 string 153 + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(string(post.MicroformatType)) 154 + if templ_7745c5c3_Err != nil { 155 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 78, Col: 57} 156 + } 157 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 158 + if templ_7745c5c3_Err != nil { 159 + return templ_7745c5c3_Err 160 + } 161 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</dd></div></dl>") 162 + if templ_7745c5c3_Err != nil { 163 + return templ_7745c5c3_Err 164 + } 165 + return templ_7745c5c3_Err 166 + }) 167 + } 168 + 169 + func PostContent(post *models.Post) templ.Component { 170 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 171 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 172 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 173 + if !templ_7745c5c3_IsBuffer { 174 + defer func() { 175 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 176 + if templ_7745c5c3_Err == nil { 177 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 178 + } 179 + }() 180 + } 181 + ctx = templ.InitializeContext(ctx) 182 + templ_7745c5c3_Var8 := templ.GetChildren(ctx) 183 + if templ_7745c5c3_Var8 == nil { 184 + templ_7745c5c3_Var8 = templ.NopComponent 134 185 } 135 186 ctx = templ.ClearChildren(ctx) 136 187 switch post.MicroformatType {
+29 -30
html/components/posts/unsupported.templ
··· 1 1 package posts 2 2 3 3 import ( 4 - "bytes" 5 - "encoding/json" 4 + "bytes" 5 + "encoding/json" 6 6 7 - "github.com/puregarlic/space/models" 8 - 9 - "github.com/alecthomas/chroma/v2/formatters/html" 10 - "github.com/alecthomas/chroma/v2/styles" 11 - "github.com/alecthomas/chroma/v2/lexers" 7 + "github.com/alecthomas/chroma/v2/formatters/html" 8 + "github.com/alecthomas/chroma/v2/lexers" 9 + "github.com/alecthomas/chroma/v2/styles" 10 + "github.com/puregarlic/space/models" 12 11 ) 13 12 14 13 var style = styles.Get("rose-pine") ··· 18 17 var dedupeSyntaxStyles = templ.NewOnceHandle() 19 18 20 19 func renderPostAsJSON(post *models.Post) string { 21 - contents, err := json.MarshalIndent(post.Properties, "", " ") 22 - if err != nil { 23 - panic(err) 24 - } 20 + contents, err := json.MarshalIndent(post.Properties, "", " ") 21 + if err != nil { 22 + panic(err) 23 + } 25 24 26 - iterator, err := lexer.Tokenise(nil, string(contents)) 25 + iterator, err := lexer.Tokenise(nil, string(contents)) 27 26 28 - var buf bytes.Buffer 29 - formatter.Format(&buf, style, iterator) 27 + var buf bytes.Buffer 28 + formatter.Format(&buf, style, iterator) 30 29 31 - return buf.String() 30 + return buf.String() 32 31 } 33 32 34 33 func generateSyntaxClassNames() string { 35 - var buf bytes.Buffer 36 - if err := formatter.WriteCSS(&buf, style); err != nil { 37 - panic(err) 38 - } 39 - 40 - return "<style>"+buf.String()+"</style>" 34 + var buf bytes.Buffer 35 + if err := formatter.WriteCSS(&buf, style); err != nil { 36 + panic(err) 37 + } 38 + 39 + return "<style>" + buf.String() + "</style>" 41 40 } 42 41 43 42 templ syntaxStyleTag() { 44 - @templ.Raw(generateSyntaxClassNames()) 43 + @templ.Raw(generateSyntaxClassNames()) 45 44 } 46 45 47 46 templ Unsupported(post *models.Post) { 48 - <div 49 - class="block p-4 bg-base overflow-x-scroll min-w-0" 50 - > 51 - @dedupeSyntaxStyles.Once() { 52 - @syntaxStyleTag() 53 - } 54 - @templ.Raw(renderPostAsJSON(post)) 55 - </div> 47 + <div 48 + class="block p-4 bg-base overflow-x-scroll min-w-0 first:rounded-t last:rounded-b border-2 border-overlay" 49 + > 50 + @dedupeSyntaxStyles.Once() { 51 + @syntaxStyleTag() 52 + } 53 + @templ.Raw(renderPostAsJSON(post)) 54 + </div> 56 55 }
+2 -3
html/components/posts/unsupported_templ.go
··· 12 12 "bytes" 13 13 "encoding/json" 14 14 15 - "github.com/puregarlic/space/models" 16 - 17 15 "github.com/alecthomas/chroma/v2/formatters/html" 18 16 "github.com/alecthomas/chroma/v2/lexers" 19 17 "github.com/alecthomas/chroma/v2/styles" 18 + "github.com/puregarlic/space/models" 20 19 ) 21 20 22 21 var style = styles.Get("rose-pine") ··· 92 91 templ_7745c5c3_Var2 = templ.NopComponent 93 92 } 94 93 ctx = templ.ClearChildren(ctx) 95 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block p-4 bg-base overflow-x-scroll min-w-0\">") 94 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block p-4 bg-base overflow-x-scroll min-w-0 first:rounded-t last:rounded-b border-2 border-overlay\">") 96 95 if templ_7745c5c3_Err != nil { 97 96 return templ_7745c5c3_Err 98 97 }
+12
html/components/sidebar.templ
··· 1 + package components 2 + 3 + templ Sidebar() { 4 + <aside class="min-w-0"> 5 + <div class="w-full md:sticky md:top-8"> 6 + <h1 class="font-extrabold text-xl">puregarlic dot space</h1> 7 + <p class="font-light mt-3 text-subtle italic md:text-sm"> 8 + this space is mine, it was <a class="underline hover:text-iris" href="https://github.com/puregarlic/space">made by me</a>! 9 + </p> 10 + </div> 11 + </aside> 12 + }
+35
html/components/sidebar_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package components 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + func Sidebar() templ.Component { 12 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 13 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 14 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 15 + if !templ_7745c5c3_IsBuffer { 16 + defer func() { 17 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 18 + if templ_7745c5c3_Err == nil { 19 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 20 + } 21 + }() 22 + } 23 + ctx = templ.InitializeContext(ctx) 24 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 25 + if templ_7745c5c3_Var1 == nil { 26 + templ_7745c5c3_Var1 = templ.NopComponent 27 + } 28 + ctx = templ.ClearChildren(ctx) 29 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<aside class=\"min-w-0\"><div class=\"w-full md:sticky md:top-8\"><h1 class=\"font-extrabold text-xl\">puregarlic dot space</h1><p class=\"font-light mt-3 text-subtle italic md:text-sm\">this space is mine, it was <a class=\"underline hover:text-iris\" href=\"https://github.com/puregarlic/space\">made by me</a>!</p></div></aside>") 30 + if templ_7745c5c3_Err != nil { 31 + return templ_7745c5c3_Err 32 + } 33 + return templ_7745c5c3_Err 34 + }) 35 + }
+15 -26
html/layouts/default.templ
··· 1 1 package layouts 2 2 3 - import "net/http" 4 - import "github.com/puregarlic/space/storage" 3 + import ( 4 + "github.com/puregarlic/space/html/components" 5 + "net/http" 6 + ) 5 7 6 - func RenderDefault(page templ.Component) http.Handler { 7 - rels := storage.GetRels() 8 - document := Default(page, rels) 8 + func RenderDefault(title string, page templ.Component) http.Handler { 9 + document := Default(title, page) 9 10 10 - return templ.Handler(document) 11 + return templ.Handler(document) 11 12 } 12 13 13 - templ Default(body templ.Component, rels []*storage.RelEntry) { 14 - <!DOCTYPE html> 15 - <html> 16 - <head> 17 - <title>Micropub and IndieAuth Server Demo</title> 18 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 19 - <link rel="preconnect" href="https://fonts.googleapis.com" /> 20 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 21 - <link href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap" rel="stylesheet"> 22 - 23 - for _, rel := range rels { 24 - <link rel={ rel.Name } href={ rel.HREF } /> 25 - } 26 - 27 - <link rel="stylesheet" href="/static/styles.css" /> 28 - </head> 29 - <body class="text-text bg-base"> 30 - @body 31 - </body> 32 - </html> 14 + templ Default(title string, body templ.Component) { 15 + <!DOCTYPE html> 16 + <html> 17 + @components.Head(title) 18 + <body class="px-4 py-12 md:py-20 text-text bg-base"> 19 + @body 20 + </body> 21 + </html> 33 22 }
+12 -39
html/layouts/default_templ.go
··· 8 8 import "github.com/a-h/templ" 9 9 import templruntime "github.com/a-h/templ/runtime" 10 10 11 - import "net/http" 12 - import "github.com/puregarlic/space/storage" 11 + import ( 12 + "github.com/puregarlic/space/html/components" 13 + "net/http" 14 + ) 13 15 14 - func RenderDefault(page templ.Component) http.Handler { 15 - rels := storage.GetRels() 16 - document := Default(page, rels) 16 + func RenderDefault(title string, page templ.Component) http.Handler { 17 + document := Default(title, page) 17 18 18 19 return templ.Handler(document) 19 20 } 20 21 21 - func Default(body templ.Component, rels []*storage.RelEntry) templ.Component { 22 + func Default(title string, body templ.Component) templ.Component { 22 23 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 23 24 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 24 25 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) ··· 36 37 templ_7745c5c3_Var1 = templ.NopComponent 37 38 } 38 39 ctx = templ.ClearChildren(ctx) 39 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><title>Micropub and IndieAuth Server Demo</title><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin><link href=\"https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&amp;display=swap\" rel=\"stylesheet\">") 40 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html>") 40 41 if templ_7745c5c3_Err != nil { 41 42 return templ_7745c5c3_Err 42 43 } 43 - for _, rel := range rels { 44 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<link rel=\"") 45 - if templ_7745c5c3_Err != nil { 46 - return templ_7745c5c3_Err 47 - } 48 - var templ_7745c5c3_Var2 string 49 - templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(rel.Name) 50 - if templ_7745c5c3_Err != nil { 51 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/layouts/default.templ`, Line: 24, Col: 28} 52 - } 53 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 54 - if templ_7745c5c3_Err != nil { 55 - return templ_7745c5c3_Err 56 - } 57 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" href=\"") 58 - if templ_7745c5c3_Err != nil { 59 - return templ_7745c5c3_Err 60 - } 61 - var templ_7745c5c3_Var3 string 62 - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(rel.HREF) 63 - if templ_7745c5c3_Err != nil { 64 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/layouts/default.templ`, Line: 24, Col: 46} 65 - } 66 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 67 - if templ_7745c5c3_Err != nil { 68 - return templ_7745c5c3_Err 69 - } 70 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") 71 - if templ_7745c5c3_Err != nil { 72 - return templ_7745c5c3_Err 73 - } 44 + templ_7745c5c3_Err = components.Head(title).Render(ctx, templ_7745c5c3_Buffer) 45 + if templ_7745c5c3_Err != nil { 46 + return templ_7745c5c3_Err 74 47 } 75 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<link rel=\"stylesheet\" href=\"/static/styles.css\"></head><body class=\"text-text bg-base\">") 48 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body class=\"px-4 py-12 md:py-20 text-text bg-base\">") 76 49 if templ_7745c5c3_Err != nil { 77 50 return templ_7745c5c3_Err 78 51 }
+29
html/layouts/sidebar.templ
··· 1 + package layouts 2 + 3 + import ( 4 + "github.com/puregarlic/space/html/components" 5 + "net/http" 6 + ) 7 + 8 + func RenderWithSidebar(title string, body templ.Component) http.Handler { 9 + page := WithSidebar(title, body) 10 + 11 + return templ.Handler(page) 12 + } 13 + 14 + templ WithSidebar(title string, body templ.Component) { 15 + <!DOCTYPE html> 16 + <html> 17 + @components.Head(title) 18 + <body class="px-4 py-12 md:py-20 text-text bg-base"> 19 + <div class="mx-auto max-w-screen-lg grid md:grid-cols-[1fr_2fr] gap-12 gap-16"> 20 + <aside class="min-w-0"> 21 + @components.Sidebar() 22 + </aside> 23 + <main class="min-w-0"> 24 + @body 25 + </main> 26 + </div> 27 + </body> 28 + </html> 29 + }
+70
html/layouts/sidebar_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package layouts 5 + 6 + //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 + 8 + import "github.com/a-h/templ" 9 + import templruntime "github.com/a-h/templ/runtime" 10 + 11 + import ( 12 + "github.com/puregarlic/space/html/components" 13 + "net/http" 14 + ) 15 + 16 + func RenderWithSidebar(title string, body templ.Component) http.Handler { 17 + page := WithSidebar(title, body) 18 + 19 + return templ.Handler(page) 20 + } 21 + 22 + func WithSidebar(title string, body templ.Component) templ.Component { 23 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 24 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 25 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 26 + if !templ_7745c5c3_IsBuffer { 27 + defer func() { 28 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 29 + if templ_7745c5c3_Err == nil { 30 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 31 + } 32 + }() 33 + } 34 + ctx = templ.InitializeContext(ctx) 35 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 36 + if templ_7745c5c3_Var1 == nil { 37 + templ_7745c5c3_Var1 = templ.NopComponent 38 + } 39 + ctx = templ.ClearChildren(ctx) 40 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html>") 41 + if templ_7745c5c3_Err != nil { 42 + return templ_7745c5c3_Err 43 + } 44 + templ_7745c5c3_Err = components.Head(title).Render(ctx, templ_7745c5c3_Buffer) 45 + if templ_7745c5c3_Err != nil { 46 + return templ_7745c5c3_Err 47 + } 48 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body class=\"px-4 py-12 md:py-20 text-text bg-base\"><div class=\"mx-auto max-w-screen-lg grid md:grid-cols-[1fr_2fr] gap-12 gap-16\"><aside class=\"min-w-0\">") 49 + if templ_7745c5c3_Err != nil { 50 + return templ_7745c5c3_Err 51 + } 52 + templ_7745c5c3_Err = components.Sidebar().Render(ctx, templ_7745c5c3_Buffer) 53 + if templ_7745c5c3_Err != nil { 54 + return templ_7745c5c3_Err 55 + } 56 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</aside><main class=\"min-w-0\">") 57 + if templ_7745c5c3_Err != nil { 58 + return templ_7745c5c3_Err 59 + } 60 + templ_7745c5c3_Err = body.Render(ctx, templ_7745c5c3_Buffer) 61 + if templ_7745c5c3_Err != nil { 62 + return templ_7745c5c3_Err 63 + } 64 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</main></div></body></html>") 65 + if templ_7745c5c3_Err != nil { 66 + return templ_7745c5c3_Err 67 + } 68 + return templ_7745c5c3_Err 69 + }) 70 + }
+59 -52
html/pages/auth.templ
··· 1 1 package pages 2 2 3 - import "strings" 3 + import ( 4 + "go.hacdias.com/indielib/indieauth" 5 + "strings" 6 + ) 4 7 5 - import "go.hacdias.com/indielib/indieauth" 6 - 7 - templ Auth( req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata ) { 8 - <!DOCTYPE html> 9 - <html> 10 - <head> 11 - <title>Authorization | Micropub and IndieAuth Server Demo</title> 12 - </head> 13 - <body> 14 - <h1>IndieAuth Server Demo: Authorization</h1> 15 - 16 - <p> 17 - You received an authorization request from 18 - 19 - if app != nil { 20 - if len(app.Logo) > 0 { 21 - <img style="width: 1em; vertical-align: middle" src={ app.Logo } /> 22 - } 23 - 24 - <strong>{ app.Name }</strong> by { app.Author }: 25 - } else { 26 - the following client: 27 - } 28 - </p> 29 - 30 - <ul> 31 - <li><strong>Redirect:</strong> <code>{ req.ClientID }</code></li> 32 - <li><strong>Client:</strong> <code>{ req.RedirectURI }</code></li> 33 - </ul> 34 - 35 - <p>For the following scopes: 36 - for _, scope := range req.Scopes { 37 - <code>{ scope }</code> 38 - } 39 - .</p> 40 - 41 - <form method='post' action='/authorization/accept'> 42 - <input type="hidden" name="response_type" value="code"> 43 - <input type="hidden" name="scope" value={ strings.Join(req.Scopes, " ") }> 44 - <input type="hidden" name="redirect_uri" value={ req.RedirectURI }> 45 - <input type="hidden" name="client_id" value={ req.ClientID }> 46 - <input type="hidden" name="state" value={ req.State }> 47 - <input type="hidden" name="code_challenge" value={ req.CodeChallenge }> 48 - <input type="hidden" name="code_challenge_method" value={ req.CodeChallengeMethod }> 49 - 50 - <p>In a production server, this page could be behind some sort of authentication mechanism, such as username and password, PassKey, etc.</p> 51 - 52 - <button id="submit">Authorize</button> 53 - </form> 54 - </body> 55 - </html> 8 + templ Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata, nonceId string, nonce string) { 9 + <main class="mx-auto max-w-screen-sm"> 10 + <p class="text-sm font-thin italic">authorize access to</p> 11 + <h1 class="mb-8 text-3xl font-extrabold">puregarlic dot space</h1> 12 + <div class="pt-6 border border-highlightMed rounded bg-surface"> 13 + if app != nil { 14 + <div class="px-6 flex gap-6 items-center"> 15 + if len(app.Logo) > 0 { 16 + <img class="max-w-12" src={ app.Logo }/> 17 + } 18 + <div> 19 + <h2 class="font-bold text-lg">{ app.Name }</h2> 20 + if len(app.Author) > 0 { 21 + <p class="text-sm font-light">by { app.Author }</p> 22 + } 23 + </div> 24 + </div> 25 + } else { 26 + <h2 class="px-6 font-bold text-subtle">unidentified client</h2> 27 + } 28 + <div class="mt-6 grid md:grid-cols-[max-content_1fr] *:border-highlightMed"> 29 + <h3 class="px-6 pt-4 pb-2 md:py-4 border-t bg-overlay md:border-r text-subtle font-bold">Client ID</h3> 30 + <p class="px-6 pb-4 pt-1 md:py-4 md:border-t bg-overlay min-w-0 overflow-x-scroll">{ req.ClientID }</p> 31 + <h3 class="px-6 pt-4 pb-2 md:py-4 border-t md:border-b md:border-r text-subtle font-bold">Redirect URL</h3> 32 + <p class="px-6 pb-4 pt-1 md:py-4 md:border-y min-w-0 overflow-x-scroll">{ req.RedirectURI }</p> 33 + <h3 class="px-6 pt-4 pb-2 md:py-4 border-t bg-overlay md:border-r text-subtle font-bold">Scopes</h3> 34 + <ul class="px-6 pb-4 pt-1 md:py-4 bg-overlay flex flex-wrap gap-3"> 35 + for _, scope := range req.Scopes { 36 + <li class="px-2 py-1 text-sm bg-pine rounded">{ scope }</li> 37 + } 38 + </ul> 39 + </div> 40 + </div> 41 + <form method="post" action="/authorization/accept"> 42 + <input type="hidden" name="response_type" value="code"/> 43 + <input type="hidden" name="scope" value={ strings.Join(req.Scopes, " ") }/> 44 + <input type="hidden" name="redirect_uri" value={ req.RedirectURI }/> 45 + <input type="hidden" name="client_id" value={ req.ClientID }/> 46 + <input type="hidden" name="state" value={ req.State }/> 47 + <input type="hidden" name="code_challenge" value={ req.CodeChallenge }/> 48 + <input type="hidden" name="code_challenge_method" value={ req.CodeChallengeMethod }/> 49 + // CSRF protections 50 + <input type="hidden" name="nonce_id" value={ nonceId }/> 51 + <input type="hidden" name="nonce" value={ nonce }/> 52 + <button 53 + class="mt-8 px-3 py-2 flex items-center justify-center gap-2 bg-surface border border-highlightMed text-sm font-bold transition rounded hover:bg-foam hover:text-surface" 54 + id="submit" 55 + > 56 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4"> 57 + <path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd"></path> 58 + </svg> 59 + Authorize 60 + </button> 61 + </form> 62 + </main> 56 63 }
+77 -36
html/pages/auth_templ.go
··· 8 8 import "github.com/a-h/templ" 9 9 import templruntime "github.com/a-h/templ/runtime" 10 10 11 - import "strings" 11 + import ( 12 + "go.hacdias.com/indielib/indieauth" 13 + "strings" 14 + ) 12 15 13 - import "go.hacdias.com/indielib/indieauth" 14 - 15 - func Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata) templ.Component { 16 + func Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata, nonceId string, nonce string) templ.Component { 16 17 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 17 18 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 18 19 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) ··· 30 31 templ_7745c5c3_Var1 = templ.NopComponent 31 32 } 32 33 ctx = templ.ClearChildren(ctx) 33 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><title>Authorization | Micropub and IndieAuth Server Demo</title></head><body><h1>IndieAuth Server Demo: Authorization</h1><p>You received an authorization request from ") 34 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<main class=\"mx-auto max-w-screen-sm\"><p class=\"text-sm font-thin italic\">authorize access to</p><h1 class=\"mb-8 text-3xl font-extrabold\">puregarlic dot space</h1><div class=\"pt-6 border border-highlightMed rounded bg-surface\">") 34 35 if templ_7745c5c3_Err != nil { 35 36 return templ_7745c5c3_Err 36 37 } 37 38 if app != nil { 39 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"px-6 flex gap-6 items-center\">") 40 + if templ_7745c5c3_Err != nil { 41 + return templ_7745c5c3_Err 42 + } 38 43 if len(app.Logo) > 0 { 39 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<img style=\"width: 1em; vertical-align: middle\" src=\"") 44 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<img class=\"max-w-12\" src=\"") 40 45 if templ_7745c5c3_Err != nil { 41 46 return templ_7745c5c3_Err 42 47 } 43 48 var templ_7745c5c3_Var2 string 44 49 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(app.Logo) 45 50 if templ_7745c5c3_Err != nil { 46 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 21, Col: 72} 51 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 16, Col: 42} 47 52 } 48 53 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 49 54 if templ_7745c5c3_Err != nil { ··· 54 59 return templ_7745c5c3_Err 55 60 } 56 61 } 57 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <strong>") 62 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div><h2 class=\"font-bold text-lg\">") 58 63 if templ_7745c5c3_Err != nil { 59 64 return templ_7745c5c3_Err 60 65 } 61 66 var templ_7745c5c3_Var3 string 62 67 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(app.Name) 63 68 if templ_7745c5c3_Err != nil { 64 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 24, Col: 26} 69 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 19, Col: 46} 65 70 } 66 71 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 67 72 if templ_7745c5c3_Err != nil { 68 73 return templ_7745c5c3_Err 69 74 } 70 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</strong> by ") 75 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h2>") 71 76 if templ_7745c5c3_Err != nil { 72 77 return templ_7745c5c3_Err 73 78 } 74 - var templ_7745c5c3_Var4 string 75 - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(app.Author) 76 - if templ_7745c5c3_Err != nil { 77 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 24, Col: 53} 78 - } 79 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 80 - if templ_7745c5c3_Err != nil { 81 - return templ_7745c5c3_Err 79 + if len(app.Author) > 0 { 80 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p class=\"text-sm font-light\">by ") 81 + if templ_7745c5c3_Err != nil { 82 + return templ_7745c5c3_Err 83 + } 84 + var templ_7745c5c3_Var4 string 85 + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(app.Author) 86 + if templ_7745c5c3_Err != nil { 87 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 21, Col: 52} 88 + } 89 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 90 + if templ_7745c5c3_Err != nil { 91 + return templ_7745c5c3_Err 92 + } 93 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>") 94 + if templ_7745c5c3_Err != nil { 95 + return templ_7745c5c3_Err 96 + } 82 97 } 83 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(":") 98 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>") 84 99 if templ_7745c5c3_Err != nil { 85 100 return templ_7745c5c3_Err 86 101 } 87 102 } else { 88 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("the following client:") 103 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h2 class=\"px-6 font-bold text-subtle\">unidentified client</h2>") 89 104 if templ_7745c5c3_Err != nil { 90 105 return templ_7745c5c3_Err 91 106 } 92 107 } 93 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><ul><li><strong>Redirect:</strong> <code>") 108 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"mt-6 grid md:grid-cols-[max-content_1fr] *:border-highlightMed\"><h3 class=\"px-6 pt-4 pb-2 md:py-4 border-t bg-overlay md:border-r text-subtle font-bold\">Client ID</h3><p class=\"px-6 pb-4 pt-1 md:py-4 md:border-t bg-overlay min-w-0 overflow-x-scroll\">") 94 109 if templ_7745c5c3_Err != nil { 95 110 return templ_7745c5c3_Err 96 111 } 97 112 var templ_7745c5c3_Var5 string 98 113 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 99 114 if templ_7745c5c3_Err != nil { 100 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 31, Col: 57} 115 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 30, Col: 101} 101 116 } 102 117 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 103 118 if templ_7745c5c3_Err != nil { 104 119 return templ_7745c5c3_Err 105 120 } 106 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</code></li><li><strong>Client:</strong> <code>") 121 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><h3 class=\"px-6 pt-4 pb-2 md:py-4 border-t md:border-b md:border-r text-subtle font-bold\">Redirect URL</h3><p class=\"px-6 pb-4 pt-1 md:py-4 md:border-y min-w-0 overflow-x-scroll\">") 107 122 if templ_7745c5c3_Err != nil { 108 123 return templ_7745c5c3_Err 109 124 } 110 125 var templ_7745c5c3_Var6 string 111 126 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 112 127 if templ_7745c5c3_Err != nil { 113 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 32, Col: 58} 128 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 32, Col: 93} 114 129 } 115 130 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 116 131 if templ_7745c5c3_Err != nil { 117 132 return templ_7745c5c3_Err 118 133 } 119 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</code></li></ul><p>For the following scopes: ") 134 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><h3 class=\"px-6 pt-4 pb-2 md:py-4 border-t bg-overlay md:border-r text-subtle font-bold\">Scopes</h3><ul class=\"px-6 pb-4 pt-1 md:py-4 bg-overlay flex flex-wrap gap-3\">") 120 135 if templ_7745c5c3_Err != nil { 121 136 return templ_7745c5c3_Err 122 137 } 123 138 for _, scope := range req.Scopes { 124 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<code>") 139 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"px-2 py-1 text-sm bg-pine rounded\">") 125 140 if templ_7745c5c3_Err != nil { 126 141 return templ_7745c5c3_Err 127 142 } 128 143 var templ_7745c5c3_Var7 string 129 144 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(scope) 130 145 if templ_7745c5c3_Err != nil { 131 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 37, Col: 21} 146 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 36, Col: 59} 132 147 } 133 148 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 134 149 if templ_7745c5c3_Err != nil { 135 150 return templ_7745c5c3_Err 136 151 } 137 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</code> ") 152 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>") 138 153 if templ_7745c5c3_Err != nil { 139 154 return templ_7745c5c3_Err 140 155 } 141 156 } 142 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(".</p><form method=\"post\" action=\"/authorization/accept\"><input type=\"hidden\" name=\"response_type\" value=\"code\"> <input type=\"hidden\" name=\"scope\" value=\"") 157 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></div></div><form method=\"post\" action=\"/authorization/accept\"><input type=\"hidden\" name=\"response_type\" value=\"code\"> <input type=\"hidden\" name=\"scope\" value=\"") 143 158 if templ_7745c5c3_Err != nil { 144 159 return templ_7745c5c3_Err 145 160 } 146 161 var templ_7745c5c3_Var8 string 147 162 templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(req.Scopes, " ")) 148 163 if templ_7745c5c3_Err != nil { 149 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 43, Col: 77} 164 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 43, Col: 74} 150 165 } 151 166 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) 152 167 if templ_7745c5c3_Err != nil { ··· 159 174 var templ_7745c5c3_Var9 string 160 175 templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 161 176 if templ_7745c5c3_Err != nil { 162 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 44, Col: 70} 177 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 44, Col: 67} 163 178 } 164 179 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) 165 180 if templ_7745c5c3_Err != nil { ··· 172 187 var templ_7745c5c3_Var10 string 173 188 templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 174 189 if templ_7745c5c3_Err != nil { 175 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 45, Col: 64} 190 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 45, Col: 61} 176 191 } 177 192 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) 178 193 if templ_7745c5c3_Err != nil { ··· 185 200 var templ_7745c5c3_Var11 string 186 201 templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(req.State) 187 202 if templ_7745c5c3_Err != nil { 188 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 46, Col: 57} 203 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 46, Col: 54} 189 204 } 190 205 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) 191 206 if templ_7745c5c3_Err != nil { ··· 198 213 var templ_7745c5c3_Var12 string 199 214 templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallenge) 200 215 if templ_7745c5c3_Err != nil { 201 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 47, Col: 74} 216 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 47, Col: 71} 202 217 } 203 218 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) 204 219 if templ_7745c5c3_Err != nil { ··· 211 226 var templ_7745c5c3_Var13 string 212 227 templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallengeMethod) 213 228 if templ_7745c5c3_Err != nil { 214 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 48, Col: 87} 229 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 48, Col: 84} 215 230 } 216 231 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) 217 232 if templ_7745c5c3_Err != nil { 218 233 return templ_7745c5c3_Err 219 234 } 220 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><p>In a production server, this page could be behind some sort of authentication mechanism, such as username and password, PassKey, etc.</p><button id=\"submit\">Authorize</button></form></body></html>") 235 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><input type=\"hidden\" name=\"nonce_id\" value=\"") 236 + if templ_7745c5c3_Err != nil { 237 + return templ_7745c5c3_Err 238 + } 239 + var templ_7745c5c3_Var14 string 240 + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(nonceId) 241 + if templ_7745c5c3_Err != nil { 242 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 50, Col: 55} 243 + } 244 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) 245 + if templ_7745c5c3_Err != nil { 246 + return templ_7745c5c3_Err 247 + } 248 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"nonce\" value=\"") 249 + if templ_7745c5c3_Err != nil { 250 + return templ_7745c5c3_Err 251 + } 252 + var templ_7745c5c3_Var15 string 253 + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(nonce) 254 + if templ_7745c5c3_Err != nil { 255 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 51, Col: 50} 256 + } 257 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) 258 + if templ_7745c5c3_Err != nil { 259 + return templ_7745c5c3_Err 260 + } 261 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <button class=\"mt-8 px-3 py-2 flex items-center justify-center gap-2 bg-surface border border-highlightMed text-sm font-bold transition rounded hover:bg-foam hover:text-surface\" id=\"submit\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"size-4\"><path fill-rule=\"evenodd\" d=\"M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z\" clip-rule=\"evenodd\"></path></svg> Authorize</button></form></main>") 221 262 if templ_7745c5c3_Err != nil { 222 263 return templ_7745c5c3_Err 223 264 }
+16 -30
html/pages/home.templ
··· 1 1 package pages 2 2 3 3 import ( 4 - "github.com/puregarlic/space/models" 5 - p "github.com/puregarlic/space/html/components/posts" 4 + p "github.com/puregarlic/space/html/components/posts" 5 + "github.com/puregarlic/space/models" 6 6 ) 7 7 8 8 templ Home(posts []*models.Post) { 9 - <div class="px-4 py-12 md:py-20 md:mx-auto md:max-w-screen-lg grid md:grid-cols-[1fr_2fr] gap-12 md:gap-16"> 10 - <aside class="min-w-0"> 11 - <div class="w-full md:sticky md:top-8"> 12 - <h1 class="font-extrabold text-xl">puregarlic dot space</h1> 13 - <p class="font-light mt-3 text-subtle italic md:text-sm"> 14 - this space is mine, it was <a class="underline hover:text-iris" href="https://github.com/puregarlic/space">made by me</a>! 15 - </p> 16 - </div> 17 - </aside> 18 - <main class="min-w-0"> 19 - <ul class="flex flex-col gap-6 md:gap-12"> 20 - if len(posts) > 0 { 21 - for _, post := range posts { 22 - <li class="flex flex-col divide-y-2 divide-highlightLow group border-2 border-highlightLow"> 23 - @p.PostContent(post) 24 - @p.PostFeedHeader(post) 25 - </li> 26 - } 27 - } else { 28 - <li class="text-muted bg-surface px-4 py-8 text-center border border-highlightLow"> 29 - intention-rich, content-poor 30 - </li> 31 - } 32 - </ul> 33 - </main> 34 - </div> 35 - 9 + <ul class="flex flex-col gap-6"> 10 + if len(posts) > 0 { 11 + for _, post := range posts { 12 + <li class="flex flex-col"> 13 + @p.PostFeedHeader(post) 14 + @p.PostContent(post) 15 + </li> 16 + } 17 + } else { 18 + <li class="text-muted bg-surface px-4 py-8 text-center border border-overlay"> 19 + intention-rich, content-poor 20 + </li> 21 + } 22 + </ul> 36 23 } 37 -
+6 -6
html/pages/home_templ.go
··· 31 31 templ_7745c5c3_Var1 = templ.NopComponent 32 32 } 33 33 ctx = templ.ClearChildren(ctx) 34 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"px-4 py-12 md:py-20 md:mx-auto md:max-w-screen-lg grid md:grid-cols-[1fr_2fr] gap-12 md:gap-16\"><aside class=\"min-w-0\"><div class=\"w-full md:sticky md:top-8\"><h1 class=\"font-extrabold text-xl\">puregarlic dot space</h1><p class=\"font-light mt-3 text-subtle italic md:text-sm\">this space is mine, it was <a class=\"underline hover:text-iris\" href=\"https://github.com/puregarlic/space\">made by me</a>!</p></div></aside><main class=\"min-w-0\"><ul class=\"flex flex-col gap-6 md:gap-12\">") 34 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<ul class=\"flex flex-col gap-6\">") 35 35 if templ_7745c5c3_Err != nil { 36 36 return templ_7745c5c3_Err 37 37 } 38 38 if len(posts) > 0 { 39 39 for _, post := range posts { 40 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"flex flex-col divide-y-2 divide-highlightLow group border-2 border-highlightLow\">") 40 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"flex flex-col\">") 41 41 if templ_7745c5c3_Err != nil { 42 42 return templ_7745c5c3_Err 43 43 } 44 - templ_7745c5c3_Err = p.PostContent(post).Render(ctx, templ_7745c5c3_Buffer) 44 + templ_7745c5c3_Err = p.PostFeedHeader(post).Render(ctx, templ_7745c5c3_Buffer) 45 45 if templ_7745c5c3_Err != nil { 46 46 return templ_7745c5c3_Err 47 47 } 48 - templ_7745c5c3_Err = p.PostFeedHeader(post).Render(ctx, templ_7745c5c3_Buffer) 48 + templ_7745c5c3_Err = p.PostContent(post).Render(ctx, templ_7745c5c3_Buffer) 49 49 if templ_7745c5c3_Err != nil { 50 50 return templ_7745c5c3_Err 51 51 } ··· 55 55 } 56 56 } 57 57 } else { 58 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"text-muted bg-surface px-4 py-8 text-center border border-highlightLow\">intention-rich, content-poor</li>") 58 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"text-muted bg-surface px-4 py-8 text-center border border-overlay\">intention-rich, content-poor</li>") 59 59 if templ_7745c5c3_Err != nil { 60 60 return templ_7745c5c3_Err 61 61 } 62 62 } 63 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></main></div>") 63 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul>") 64 64 if templ_7745c5c3_Err != nil { 65 65 return templ_7745c5c3_Err 66 66 }
+22 -56
html/pages/post.templ
··· 1 1 package pages 2 2 3 - import "github.com/puregarlic/space/models" 4 - import "fmt" 5 - import "encoding/json" 6 - import "reflect" 7 - 8 - func printProperty(post *models.Post, name string) string { 9 - props := make(map[string]any) 10 - if err := json.Unmarshal(post.Properties, &props); err != nil { 11 - panic(err) 12 - } 13 - 14 - if val, ok := props[name]; ok { 15 - tp := reflect.TypeOf(val) 16 - switch tp.Kind() { 17 - default: 18 - return fmt.Sprint(val) 19 - case reflect.Slice: 20 - str := "" 21 - for _, v := range val.([]any) { 22 - str = str + fmt.Sprint(v) 23 - } 24 - 25 - return str 26 - } 27 - 28 - } 29 - 30 - return "<no name provided>" 31 - } 32 - 33 - func printPost(post *models.Post) string { 34 - out, err := json.Marshal(post) 35 - 36 - if (err != nil) { 37 - panic (err) 38 - } 39 - 40 - return fmt.Sprint(string(out)) 41 - } 3 + import ( 4 + "github.com/puregarlic/space/html/components/posts" 5 + "github.com/puregarlic/space/models" 6 + ) 42 7 43 8 templ Post(post *models.Post) { 44 - <!DOCTYPE html> 45 - <html> 46 - <head> 47 - <title>Post | Micropub and IndieAuth Server Demo</title> 48 - </head> 49 - <body> 50 - <div class={ post.Type }> 51 - <h1 class="p-name">{ printProperty(post, "name") }</h1> 52 - <p class="p-content">{ printProperty(post, "content") }</p> 53 - 54 - <h3>Stored Microformats</h3> 55 - <code> 56 - <pre>{ printPost(post) }</pre> 57 - </code> 58 - </div> 59 - </body> 60 - </html> 9 + <div class="flex flex-col gap-8"> 10 + <a href="/" class="text-sm text-muted flex items-center gap-1"> 11 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4"> 12 + <path fill-rule="evenodd" d="M14 8a.75.75 0 0 1-.75.75H4.56l1.22 1.22a.75.75 0 1 1-1.06 1.06l-2.5-2.5a.75.75 0 0 1 0-1.06l2.5-2.5a.75.75 0 0 1 1.06 1.06L4.56 7.25h8.69A.75.75 0 0 1 14 8Z" clip-rule="evenodd"></path> 13 + </svg> 14 + back to home 15 + </a> 16 + <div> 17 + @posts.PostContent(post) 18 + </div> 19 + @posts.PostDetails(post) 20 + <div class="py-12 flex flex-col gap-1 items-center text-muted text-xs font-light"> 21 + interactions not implemented yet 22 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4"> 23 + <path fill-rule="evenodd" d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0ZM6 8c.552 0 1-.672 1-1.5S6.552 5 6 5s-1 .672-1 1.5S5.448 8 6 8Zm5-1.5c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5Zm-6.005 5.805a.75.75 0 0 0 1.06 0 2.75 2.75 0 0 1 3.89 0 .75.75 0 0 0 1.06-1.06 4.25 4.25 0 0 0-6.01 0 .75.75 0 0 0 0 1.06Z" clip-rule="evenodd"></path> 24 + </svg> 25 + </div> 26 + </div> 61 27 }
+9 -89
html/pages/post_templ.go
··· 8 8 import "github.com/a-h/templ" 9 9 import templruntime "github.com/a-h/templ/runtime" 10 10 11 - import "github.com/puregarlic/space/models" 12 - import "fmt" 13 - import "encoding/json" 14 - import "reflect" 15 - 16 - func printProperty(post *models.Post, name string) string { 17 - props := make(map[string]any) 18 - if err := json.Unmarshal(post.Properties, &props); err != nil { 19 - panic(err) 20 - } 21 - 22 - if val, ok := props[name]; ok { 23 - tp := reflect.TypeOf(val) 24 - switch tp.Kind() { 25 - default: 26 - return fmt.Sprint(val) 27 - case reflect.Slice: 28 - str := "" 29 - for _, v := range val.([]any) { 30 - str = str + fmt.Sprint(v) 31 - } 32 - 33 - return str 34 - } 35 - 36 - } 37 - 38 - return "<no name provided>" 39 - } 40 - 41 - func printPost(post *models.Post) string { 42 - out, err := json.Marshal(post) 43 - 44 - if err != nil { 45 - panic(err) 46 - } 47 - 48 - return fmt.Sprint(string(out)) 49 - } 11 + import ( 12 + "github.com/puregarlic/space/html/components/posts" 13 + "github.com/puregarlic/space/models" 14 + ) 50 15 51 16 func Post(post *models.Post) templ.Component { 52 17 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { ··· 66 31 templ_7745c5c3_Var1 = templ.NopComponent 67 32 } 68 33 ctx = templ.ClearChildren(ctx) 69 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><title>Post | Micropub and IndieAuth Server Demo</title></head><body>") 70 - if templ_7745c5c3_Err != nil { 71 - return templ_7745c5c3_Err 72 - } 73 - var templ_7745c5c3_Var2 = []any{post.Type} 74 - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) 34 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex flex-col gap-8\"><a href=\"/\" class=\"text-sm text-muted flex items-center gap-1\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"size-4\"><path fill-rule=\"evenodd\" d=\"M14 8a.75.75 0 0 1-.75.75H4.56l1.22 1.22a.75.75 0 1 1-1.06 1.06l-2.5-2.5a.75.75 0 0 1 0-1.06l2.5-2.5a.75.75 0 0 1 1.06 1.06L4.56 7.25h8.69A.75.75 0 0 1 14 8Z\" clip-rule=\"evenodd\"></path></svg> back to home</a><div>") 75 35 if templ_7745c5c3_Err != nil { 76 36 return templ_7745c5c3_Err 77 37 } 78 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"") 38 + templ_7745c5c3_Err = posts.PostContent(post).Render(ctx, templ_7745c5c3_Buffer) 79 39 if templ_7745c5c3_Err != nil { 80 40 return templ_7745c5c3_Err 81 41 } 82 - var templ_7745c5c3_Var3 string 83 - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String()) 84 - if templ_7745c5c3_Err != nil { 85 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/post.templ`, Line: 1, Col: 0} 86 - } 87 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 42 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") 88 43 if templ_7745c5c3_Err != nil { 89 44 return templ_7745c5c3_Err 90 45 } 91 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><h1 class=\"p-name\">") 46 + templ_7745c5c3_Err = posts.PostDetails(post).Render(ctx, templ_7745c5c3_Buffer) 92 47 if templ_7745c5c3_Err != nil { 93 48 return templ_7745c5c3_Err 94 49 } 95 - var templ_7745c5c3_Var4 string 96 - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(printProperty(post, "name")) 97 - if templ_7745c5c3_Err != nil { 98 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/post.templ`, Line: 51, Col: 56} 99 - } 100 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 101 - if templ_7745c5c3_Err != nil { 102 - return templ_7745c5c3_Err 103 - } 104 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1><p class=\"p-content\">") 105 - if templ_7745c5c3_Err != nil { 106 - return templ_7745c5c3_Err 107 - } 108 - var templ_7745c5c3_Var5 string 109 - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(printProperty(post, "content")) 110 - if templ_7745c5c3_Err != nil { 111 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/post.templ`, Line: 52, Col: 61} 112 - } 113 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 114 - if templ_7745c5c3_Err != nil { 115 - return templ_7745c5c3_Err 116 - } 117 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><h3>Stored Microformats</h3><code><pre>") 118 - if templ_7745c5c3_Err != nil { 119 - return templ_7745c5c3_Err 120 - } 121 - var templ_7745c5c3_Var6 string 122 - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(printPost(post)) 123 - if templ_7745c5c3_Err != nil { 124 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/post.templ`, Line: 56, Col: 32} 125 - } 126 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 127 - if templ_7745c5c3_Err != nil { 128 - return templ_7745c5c3_Err 129 - } 130 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</pre></code></div></body></html>") 50 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"py-12 flex flex-col gap-1 items-center text-muted text-xs font-light\">interactions not implemented yet <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" class=\"size-4\"><path fill-rule=\"evenodd\" d=\"M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0ZM6 8c.552 0 1-.672 1-1.5S6.552 5 6 5s-1 .672-1 1.5S5.448 8 6 8Zm5-1.5c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5Zm-6.005 5.805a.75.75 0 0 0 1.06 0 2.75 2.75 0 0 1 3.89 0 .75.75 0 0 0 1.06-1.06 4.25 4.25 0 0 0-6.01 0 .75.75 0 0 0 0 1.06Z\" clip-rule=\"evenodd\"></path></svg></div></div>") 131 51 if templ_7745c5c3_Err != nil { 132 52 return templ_7745c5c3_Err 133 53 }
+1 -1
main.go
··· 27 27 28 28 func main() { 29 29 port, profileURL := validateRuntimeConfiguration() 30 - defer storage.CleanupAuthCache() 30 + defer storage.CleanupCaches() 31 31 32 32 storage.GORM().AutoMigrate(&models.Post{}) 33 33
+4
models/post.go
··· 19 19 UpdatedAt time.Time 20 20 DeletedAt gorm.DeletedAt `gorm:"index"` 21 21 } 22 + 23 + func (p *Post) Timestamp() string { 24 + return p.CreatedAt.Format("01/02/2006 at 3:04 PM") 25 + }
+14 -29
services/indieauth.go
··· 26 26 Server *indieauth.Server 27 27 } 28 28 29 - // storeAuthorization stores the authorization request and returns a code for it. 30 - // Something such as JWT tokens could be used in a production environment. 31 29 func (i *IndieAuth) storeAuthorization(req *indieauth.AuthenticationRequest) string { 32 30 code := nanoid.New() 33 31 ··· 47 45 scopesContextKey contextKey = "scopes" 48 46 ) 49 47 50 - // HandleAuthGET handles the GET method for the authorization endpoint. 51 48 func (i *IndieAuth) HandleAuthGET(w http.ResponseWriter, r *http.Request) { 52 - // In a production server, this page would usually be protected. In order for 53 - // the user to authorize this request, they must be authenticated. This could 54 - // be done in different ways: username/password, passkeys, etc. 55 - 56 - // Parse the authorization request. 57 49 req, err := i.Server.ParseAuthorization(r) 58 50 if err != nil { 59 51 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 60 52 return 61 53 } 62 54 63 - // Do a best effort attempt at fetching more information about the application 64 - // that we can show to the user. Not all applications provide this sort of 65 - // information. 66 55 app, _ := i.Server.DiscoverApplicationMetadata(r.Context(), req.ClientID) 67 56 68 - // Here, we just display a small HTML document where the user has to press 69 - // to authorize this request. Please note that this template contains a form 70 - // where we dump all the request information. This makes it possible to reuse 71 - // [indieauth.Server.ParseAuthorization] when the user authorizes the request. 72 - layouts.RenderDefault(pages.Auth(req, app)).ServeHTTP(w, r) 57 + nonceId, nonce := nanoid.New(), nanoid.New() 58 + storage.NonceCache().Set(nonceId, nonce, 0) 59 + 60 + layouts.RenderDefault("authorize", pages.Auth(req, app, nonceId, nonce)).ServeHTTP(w, r) 73 61 } 74 62 75 - // HandleAuthPOST handles the POST method for the authorization endpoint. 76 63 func (i *IndieAuth) HandleAuthPOST(w http.ResponseWriter, r *http.Request) { 77 64 i.authorizationCodeExchange(w, r, false) 78 65 } 79 66 80 - // HandleToken handles the token endpoint. In our case, we only accept the default 81 - // type which is exchanging an authorization code for a token. 82 67 func (i *IndieAuth) HandleToken(w http.ResponseWriter, r *http.Request) { 83 68 if r.Method != http.MethodPost { 84 69 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) ··· 103 88 ExpiresIn int64 `json:"expires_in,omitempty"` 104 89 } 105 90 106 - // authorizationCodeExchange handles the authorization code exchange. It is used by 107 - // both the authorization handler to exchange the code for the user's profile URL, 108 - // and by the token endpoint, to exchange the code by a token. 109 91 func (i *IndieAuth) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) { 110 92 if err := r.ParseForm(); err != nil { 111 93 SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) ··· 163 145 } 164 146 165 147 func (i *IndieAuth) HandleAuthApproval(w http.ResponseWriter, r *http.Request) { 166 - // Parse authorization information. This only works because our authorization page 167 - // includes all the required information. This can be done in other ways: database, 168 - // whether temporary or not, cookies, etc. 148 + id := r.FormValue("nonce_id") 149 + nonce := r.FormValue("nonce") 150 + 151 + stored, ok := storage.NonceCache().GetAndDelete(id) 152 + if !ok { 153 + SendErrorJSON(w, http.StatusBadRequest, "bad_request", "nonce does not match") 154 + } else if stored.Value() != nonce { 155 + SendErrorJSON(w, http.StatusBadRequest, "bad_request", "nonce does not match") 156 + } 157 + 169 158 req, err := i.Server.ParseAuthorization(r) 170 159 if err != nil { 171 160 SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 172 161 return 173 162 } 174 163 175 - // Generate a random code and persist the information associated to that code. 176 - // You could do this in other ways: database, or JWT tokens, or both, for example. 177 164 code := i.storeAuthorization(req) 178 165 179 166 // Redirect to client callback. ··· 183 170 http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound) 184 171 } 185 172 186 - // MustAuth is a middleware to ensure that the request is authorized. The way this 187 - // works depends on the implementation. It then stores the scopes in the context. 188 173 func MustAuth(next http.Handler) http.Handler { 189 174 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 190 175 tokenStr := r.Header.Get("Authorization")
+18 -1
storage/cache.go
··· 8 8 ) 9 9 10 10 var authCache *ttlcache.Cache[string, *indieauth.AuthenticationRequest] 11 + var nonceCache *ttlcache.Cache[string, string] 11 12 12 - func CleanupAuthCache() { 13 + func CleanupCaches() { 13 14 AuthCache().Stop() 14 15 } 15 16 ··· 28 29 29 30 return cache 30 31 } 32 + 33 + func NonceCache() *ttlcache.Cache[string, string] { 34 + if nonceCache != nil { 35 + return nonceCache 36 + } 37 + 38 + cache := ttlcache.New( 39 + ttlcache.WithTTL[string, string](5 * time.Minute), 40 + ) 41 + 42 + go cache.Start() 43 + 44 + nonceCache = cache 45 + 46 + return cache 47 + }