An experimental IndieWeb site built in Go.

implement layout system for templates

Required a lot of refactoring to avoid cyclical imports, but we got
there in the end

Closes #1

+314 -148
-23
handlers/http.go
··· 1 - package handlers 2 - 3 - import ( 4 - "encoding/json" 5 - "net/http" 6 - ) 7 - 8 - func SendHttpError(w http.ResponseWriter, status int) { 9 - http.Error(w, http.StatusText(status), status) 10 - } 11 - 12 - func SendJSON(w http.ResponseWriter, code int, data interface{}) { 13 - w.Header().Set("Content-Type", "application/json; charset=utf-8") 14 - w.WriteHeader(code) 15 - _ = json.NewEncoder(w).Encode(data) 16 - } 17 - 18 - func SendErrorJSON(w http.ResponseWriter, code int, err, errDescription string) { 19 - SendJSON(w, code, map[string]string{ 20 - "error": err, 21 - "error_description": errDescription, 22 - }) 23 - }
+2 -4
handlers/media.go
··· 21 21 }) 22 22 if err != nil { 23 23 fmt.Println("failed to get object", err) 24 - SendHttpError(w, http.StatusInternalServerError) 25 - return 24 + panic(err) 26 25 } 27 26 28 27 defer res.Body.Close() ··· 31 30 32 31 if _, err := io.Copy(w, res.Body); err != nil { 33 32 fmt.Println("failed to send object", err) 34 - SendHttpError(w, http.StatusInternalServerError) 35 - return 33 + panic(err) 36 34 } 37 35 }
+6 -5
handlers/routes.go
··· 3 3 import ( 4 4 "net/http" 5 5 6 - "github.com/a-h/templ" 7 6 "github.com/go-chi/chi/v5" 8 7 "github.com/puregarlic/space/models" 9 - "github.com/puregarlic/space/pages" 10 8 "github.com/puregarlic/space/storage" 9 + 10 + "github.com/puregarlic/space/html/layouts" 11 + "github.com/puregarlic/space/html/pages" 11 12 ) 12 13 13 14 func ServeHomePage(w http.ResponseWriter, r *http.Request) { ··· 17 18 panic(result.Error) 18 19 } 19 20 20 - templ.Handler(pages.Home(posts)).ServeHTTP(w, r) 21 + layouts.RenderDefault(pages.Home(posts)).ServeHTTP(w, r) 21 22 } 22 23 23 24 func ServePostPage(w http.ResponseWriter, r *http.Request) { ··· 27 28 result := storage.GORM().First(post, "id = ?", id) 28 29 29 30 if result.RowsAffected == 0 { 30 - SendHttpError(w, http.StatusNotFound) 31 + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 31 32 return 32 33 } 33 34 34 - templ.Handler(pages.Post(post)).ServeHTTP(w, r) 35 + layouts.RenderDefault(pages.Post(post)).ServeHTTP(w, r) 35 36 }
+87
handlers/services.go
··· 1 + package handlers 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/puregarlic/space/services" 7 + "github.com/puregarlic/space/storage" 8 + 9 + "github.com/go-chi/chi/v5" 10 + "github.com/go-chi/cors" 11 + "go.hacdias.com/indielib/indieauth" 12 + "go.hacdias.com/indielib/microformats" 13 + "go.hacdias.com/indielib/micropub" 14 + ) 15 + 16 + func AttachIndieAuth(r chi.Router, path string, profileUrl string) { 17 + svc := &services.IndieAuth{ 18 + ProfileURL: profileUrl, 19 + Server: indieauth.NewServer(true, nil), 20 + } 21 + 22 + r.Route(path, func(r chi.Router) { 23 + r.Post("/", svc.HandleAuthPOST) 24 + r.Post("/token", svc.HandleToken) 25 + r.Post("/accept", svc.HandleAuthApproval) 26 + 27 + // User authentication portal 28 + r.With(services.MustBasicAuth).Get("/", svc.HandleAuthGET) 29 + }) 30 + 31 + storage.AddRel("authorization_endpoint", path) 32 + storage.AddRel("token_endpoint", path+"/token") 33 + } 34 + 35 + func AttachMicropub(r chi.Router, path string, profileURL string) { 36 + mp := &services.Micropub{ 37 + ProfileURL: profileURL, 38 + } 39 + 40 + mpHandler := micropub.NewHandler( 41 + mp, 42 + micropub.WithMediaEndpoint(profileURL+"micropub/media"), 43 + micropub.WithGetPostTypes(func() []micropub.PostType { 44 + return []micropub.PostType{ 45 + { 46 + Name: "Post", 47 + Type: string(microformats.TypeNote), 48 + }, 49 + { 50 + Name: "Photo", 51 + Type: string(microformats.TypePhoto), 52 + }, 53 + } 54 + }), 55 + ) 56 + 57 + r.Route(path, func(r chi.Router) { 58 + // Enable CORS for browser-based clients 59 + r.Use(cors.Handler( 60 + cors.Options{ 61 + AllowedHeaders: []string{ 62 + "Accept", 63 + "Authorization", 64 + "Content-Type", 65 + }, 66 + }, 67 + )) 68 + 69 + // Require access tokens for all Micropub routes 70 + r.Use(services.MustAuth) 71 + 72 + r.Get("/", mpHandler.ServeHTTP) 73 + r.Post("/", mpHandler.ServeHTTP) 74 + r.Post("/media", micropub.NewMediaHandler( 75 + mp.HandleMediaUpload, 76 + func(r *http.Request, scope string) bool { 77 + // IndieKit checks for a `media` scope, not commonly requested 78 + hasMediaScope := mp.HasScope(r, scope) 79 + hasCreateScope := mp.HasScope(r, "create") 80 + 81 + return hasMediaScope || hasCreateScope 82 + }, 83 + ).ServeHTTP) 84 + }) 85 + 86 + storage.AddRel("micropub", path) 87 + }
+29
html/layouts/default.templ
··· 1 + package layouts 2 + 3 + import "net/http" 4 + import "github.com/puregarlic/space/storage" 5 + 6 + func RenderDefault(page templ.Component) http.Handler { 7 + rels := storage.GetRels() 8 + document := Default(page, rels) 9 + 10 + return templ.Handler(document) 11 + } 12 + 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 + 19 + for _, rel := range rels { 20 + <link rel={ rel.Name } href={ rel.HREF } /> 21 + } 22 + 23 + <link rel="stylesheet" href="/static/styles.css" /> 24 + </head> 25 + <body> 26 + @body 27 + </body> 28 + </html> 29 + }
+89
html/layouts/default_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 "net/http" 12 + import "github.com/puregarlic/space/storage" 13 + 14 + func RenderDefault(page templ.Component) http.Handler { 15 + rels := storage.GetRels() 16 + document := Default(page, rels) 17 + 18 + return templ.Handler(document) 19 + } 20 + 21 + func Default(body templ.Component, rels []*storage.RelEntry) templ.Component { 22 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 23 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 24 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 25 + if !templ_7745c5c3_IsBuffer { 26 + defer func() { 27 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 28 + if templ_7745c5c3_Err == nil { 29 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 30 + } 31 + }() 32 + } 33 + ctx = templ.InitializeContext(ctx) 34 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 35 + if templ_7745c5c3_Var1 == nil { 36 + templ_7745c5c3_Var1 = templ.NopComponent 37 + } 38 + ctx = templ.ClearChildren(ctx) 39 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><title>Micropub and IndieAuth Server Demo</title>") 40 + if templ_7745c5c3_Err != nil { 41 + return templ_7745c5c3_Err 42 + } 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: 20, 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: 20, 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 + } 74 + } 75 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<link rel=\"stylesheet\" href=\"/static/styles.css\"></head><body>") 76 + if templ_7745c5c3_Err != nil { 77 + return templ_7745c5c3_Err 78 + } 79 + templ_7745c5c3_Err = body.Render(ctx, templ_7745c5c3_Buffer) 80 + if templ_7745c5c3_Err != nil { 81 + return templ_7745c5c3_Err 82 + } 83 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>") 84 + if templ_7745c5c3_Err != nil { 85 + return templ_7745c5c3_Err 86 + } 87 + return templ_7745c5c3_Err 88 + }) 89 + }
+39 -86
main.go
··· 12 12 "strconv" 13 13 14 14 "github.com/puregarlic/space/handlers" 15 - "github.com/puregarlic/space/services" 16 15 "github.com/puregarlic/space/storage" 17 16 18 17 "github.com/go-chi/chi/v5" ··· 21 20 "github.com/go-chi/httprate" 22 21 23 22 "go.hacdias.com/indielib/indieauth" 24 - "go.hacdias.com/indielib/microformats" 25 - "go.hacdias.com/indielib/micropub" 26 23 27 24 _ "github.com/joho/godotenv/autoload" 28 25 ) 29 26 30 27 func main() { 28 + port, profileURL := validateRuntimeConfiguration() 29 + defer storage.CleanupAuthCache() 30 + 31 + r := chi.NewRouter() 32 + 33 + r.Use(middleware.RequestID) 34 + r.Use(middleware.RealIP) 35 + r.Use(middleware.Logger) 36 + r.Use(middleware.Recoverer) 37 + r.Use(httprate.LimitByIP(100, 1*time.Minute)) 38 + 39 + // CORS be enabled for browser-based agents to fetch `rel` elements. 40 + // We'll enable it just on the root route since it should be used as the profile URL 41 + r.With(cors.AllowAll().Handler).Get("/", handlers.ServeHomePage) 42 + 43 + // Content pages 44 + r.Get("/posts/{slug}", handlers.ServePostPage) 45 + 46 + // Static asset handlers 47 + r.Get("/media/*", handlers.ServeMedia) 48 + r.Get("/static/*", http.StripPrefix( 49 + "/static", 50 + http.FileServer(http.Dir("static")), 51 + ).ServeHTTP) 52 + 53 + // Service handlers 54 + handlers.AttachIndieAuth(r, "/authorization", profileURL) 55 + handlers.AttachMicropub(r, "/micropub", profileURL) 56 + 57 + // Start it! 58 + log.Printf("Listening on http://localhost:%d", port) 59 + log.Printf("Listening on %s", profileURL) 60 + if err := http.ListenAndServe(":"+strconv.Itoa(port), r); err != nil { 61 + log.Fatal(err) 62 + } 63 + } 64 + 65 + func validateRuntimeConfiguration() (portNumber int, profileURL string) { 31 66 var port int 32 67 if portStr, ok := os.LookupEnv("PORT"); !ok { 33 68 port = 80 ··· 51 86 log.Fatal(err) 52 87 } 53 88 54 - defer storage.CleanupAuthCache() 55 - 56 - r := chi.NewRouter() 57 - 58 - r.Use(middleware.RequestID) 59 - r.Use(middleware.RealIP) 60 - r.Use(middleware.Logger) 61 - r.Use(middleware.Recoverer) 62 - r.Use(httprate.LimitByIP(100, 1*time.Minute)) 63 - 64 - // Static resources 65 - r.Get("/static/*", http.StripPrefix("/static", http.FileServer(http.Dir("static"))).ServeHTTP) 66 - 67 - // Pages 68 - r.Get("/", handlers.ServeHomePage) 69 - r.Get("/posts/{slug}", handlers.ServePostPage) 70 - r.Get("/media/*", handlers.ServeMedia) 71 - 72 - // IndieAuth handlers 73 - r.Group(func(r chi.Router) { 74 - ias := &services.IndieAuth{ 75 - ProfileURL: profileURL, 76 - Server: indieauth.NewServer(true, nil), 77 - } 78 - 79 - r.Post("/token", ias.HandleToken) 80 - r.Post("/authorization", ias.HandleAuthPOST) 81 - r.Post("/authorization/accept", ias.HandleAuthApproval) 82 - 83 - // User authentication portal 84 - r.With(services.MustBasicAuth).Get("/authorization", ias.HandleAuthGET) 85 - }) 86 - 87 - // Micropub handler 88 - r.Route("/micropub", func(r chi.Router) { 89 - // Enable CORS for browser-based clients 90 - r.Use(cors.Handler( 91 - cors.Options{ 92 - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, 93 - }, 94 - )) 95 - r.Use(services.MustAuth) 96 - 97 - mp := &services.Micropub{ 98 - ProfileURL: profileURL, 99 - } 100 - mpHandler := micropub.NewHandler( 101 - mp, 102 - micropub.WithGetPostTypes(func() []micropub.PostType { 103 - return []micropub.PostType{ 104 - { 105 - Name: "Post", 106 - Type: string(microformats.TypeNote), 107 - }, 108 - { 109 - Name: "Photo", 110 - Type: string(microformats.TypePhoto), 111 - }, 112 - } 113 - }), 114 - micropub.WithMediaEndpoint(profileURL+"micropub/media"), 115 - ) 116 - 117 - r.Get("/", mpHandler.ServeHTTP) 118 - r.Post("/", mpHandler.ServeHTTP) 119 - r.Post("/media", micropub.NewMediaHandler( 120 - mp.HandleMediaUpload, 121 - func(r *http.Request, scope string) bool { 122 - // IndieKit checks for a `media` scope, not commonly requested 123 - hasMediaScope := mp.HasScope(r, scope) 124 - hasCreateScope := mp.HasScope(r, "create") 125 - 126 - return hasMediaScope || hasCreateScope 127 - }, 128 - ).ServeHTTP) 129 - }) 130 - 131 - // Start it! 132 - log.Printf("Listening on http://localhost:%d", port) 133 - log.Printf("Listening on %s", profileURL) 134 - if err := http.ListenAndServe(":"+strconv.Itoa(port), r); err != nil { 135 - log.Fatal(err) 136 - } 89 + return port, profileURL 137 90 }
pages/auth.templ html/pages/auth.templ
+12 -12
pages/auth_templ.go html/pages/auth_templ.go
··· 43 43 var templ_7745c5c3_Var2 string 44 44 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(app.Logo) 45 45 if templ_7745c5c3_Err != nil { 46 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 21, Col: 72} 46 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 21, Col: 72} 47 47 } 48 48 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 49 49 if templ_7745c5c3_Err != nil { ··· 61 61 var templ_7745c5c3_Var3 string 62 62 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(app.Name) 63 63 if templ_7745c5c3_Err != nil { 64 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 24, Col: 26} 64 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 24, Col: 26} 65 65 } 66 66 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 67 67 if templ_7745c5c3_Err != nil { ··· 74 74 var templ_7745c5c3_Var4 string 75 75 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(app.Author) 76 76 if templ_7745c5c3_Err != nil { 77 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 24, Col: 53} 77 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 24, Col: 53} 78 78 } 79 79 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 80 80 if templ_7745c5c3_Err != nil { ··· 97 97 var templ_7745c5c3_Var5 string 98 98 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 99 99 if templ_7745c5c3_Err != nil { 100 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 31, Col: 57} 100 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 31, Col: 57} 101 101 } 102 102 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 103 103 if templ_7745c5c3_Err != nil { ··· 110 110 var templ_7745c5c3_Var6 string 111 111 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 112 112 if templ_7745c5c3_Err != nil { 113 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 32, Col: 58} 113 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 32, Col: 58} 114 114 } 115 115 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 116 116 if templ_7745c5c3_Err != nil { ··· 128 128 var templ_7745c5c3_Var7 string 129 129 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(scope) 130 130 if templ_7745c5c3_Err != nil { 131 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 37, Col: 21} 131 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 37, Col: 21} 132 132 } 133 133 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 134 134 if templ_7745c5c3_Err != nil { ··· 146 146 var templ_7745c5c3_Var8 string 147 147 templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(req.Scopes, " ")) 148 148 if templ_7745c5c3_Err != nil { 149 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 43, Col: 77} 149 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 43, Col: 77} 150 150 } 151 151 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) 152 152 if templ_7745c5c3_Err != nil { ··· 159 159 var templ_7745c5c3_Var9 string 160 160 templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 161 161 if templ_7745c5c3_Err != nil { 162 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 44, Col: 70} 162 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 44, Col: 70} 163 163 } 164 164 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) 165 165 if templ_7745c5c3_Err != nil { ··· 172 172 var templ_7745c5c3_Var10 string 173 173 templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 174 174 if templ_7745c5c3_Err != nil { 175 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 45, Col: 64} 175 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 45, Col: 64} 176 176 } 177 177 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) 178 178 if templ_7745c5c3_Err != nil { ··· 185 185 var templ_7745c5c3_Var11 string 186 186 templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(req.State) 187 187 if templ_7745c5c3_Err != nil { 188 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 46, Col: 57} 188 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 46, Col: 57} 189 189 } 190 190 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) 191 191 if templ_7745c5c3_Err != nil { ··· 198 198 var templ_7745c5c3_Var12 string 199 199 templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallenge) 200 200 if templ_7745c5c3_Err != nil { 201 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 47, Col: 74} 201 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 47, Col: 74} 202 202 } 203 203 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) 204 204 if templ_7745c5c3_Err != nil { ··· 211 211 var templ_7745c5c3_Var13 string 212 212 templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallengeMethod) 213 213 if templ_7745c5c3_Err != nil { 214 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 48, Col: 87} 214 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 48, Col: 87} 215 215 } 216 216 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) 217 217 if templ_7745c5c3_Err != nil {
pages/home.templ html/pages/home.templ
+2 -2
pages/home_templ.go html/pages/home_templ.go
··· 60 60 var templ_7745c5c3_Var3 string 61 61 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(post.ID.String()) 62 62 if templ_7745c5c3_Err != nil { 63 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/home.templ`, Line: 35, Col: 82} 63 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/home.templ`, Line: 35, Col: 82} 64 64 } 65 65 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 66 66 if templ_7745c5c3_Err != nil { ··· 73 73 var templ_7745c5c3_Var4 string 74 74 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(getPostPropertyValue(post, "content"))) 75 75 if templ_7745c5c3_Err != nil { 76 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/home.templ`, Line: 35, Col: 142} 76 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/home.templ`, Line: 35, Col: 142} 77 77 } 78 78 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 79 79 if templ_7745c5c3_Err != nil {
pages/post.templ html/pages/post.templ
+4 -4
pages/post_templ.go html/pages/post_templ.go
··· 82 82 var templ_7745c5c3_Var3 string 83 83 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String()) 84 84 if templ_7745c5c3_Err != nil { 85 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/post.templ`, Line: 1, Col: 0} 85 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/post.templ`, Line: 1, Col: 0} 86 86 } 87 87 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 88 88 if templ_7745c5c3_Err != nil { ··· 95 95 var templ_7745c5c3_Var4 string 96 96 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(printProperty(post, "name")) 97 97 if templ_7745c5c3_Err != nil { 98 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/post.templ`, Line: 51, Col: 56} 98 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/post.templ`, Line: 51, Col: 56} 99 99 } 100 100 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 101 101 if templ_7745c5c3_Err != nil { ··· 108 108 var templ_7745c5c3_Var5 string 109 109 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(printProperty(post, "content")) 110 110 if templ_7745c5c3_Err != nil { 111 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/post.templ`, Line: 52, Col: 61} 111 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/post.templ`, Line: 52, Col: 61} 112 112 } 113 113 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 114 114 if templ_7745c5c3_Err != nil { ··· 121 121 var templ_7745c5c3_Var6 string 122 122 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(printPost(post)) 123 123 if templ_7745c5c3_Err != nil { 124 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/post.templ`, Line: 56, Col: 32} 124 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/post.templ`, Line: 56, Col: 32} 125 125 } 126 126 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 127 127 if templ_7745c5c3_Err != nil {
+25 -12
services/indieauth.go
··· 4 4 "context" 5 5 "crypto/sha256" 6 6 "crypto/subtle" 7 + "encoding/json" 7 8 "errors" 8 9 "net/http" 9 10 "net/url" ··· 11 12 "strings" 12 13 "time" 13 14 14 - "github.com/puregarlic/space/handlers" 15 - "github.com/puregarlic/space/pages" 15 + "github.com/puregarlic/space/html/layouts" 16 + "github.com/puregarlic/space/html/pages" 16 17 "github.com/puregarlic/space/storage" 17 18 18 - "github.com/a-h/templ" 19 19 "github.com/aidarkhanov/nanoid" 20 20 "github.com/golang-jwt/jwt/v5" 21 21 "go.hacdias.com/indielib/indieauth" ··· 69 69 // to authorize this request. Please note that this template contains a form 70 70 // where we dump all the request information. This makes it possible to reuse 71 71 // [indieauth.Server.ParseAuthorization] when the user authorizes the request. 72 - templ.Handler(pages.Auth(req, app)).ServeHTTP(w, r) 72 + layouts.RenderDefault(pages.Auth(req, app)).ServeHTTP(w, r) 73 73 } 74 74 75 75 // HandleAuthPOST handles the POST method for the authorization endpoint. ··· 108 108 // and by the token endpoint, to exchange the code by a token. 109 109 func (i *IndieAuth) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) { 110 110 if err := r.ParseForm(); err != nil { 111 - handlers.SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 111 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 112 112 return 113 113 } 114 114 115 115 // t := s.getAuthorization(r.Form.Get("code")) 116 116 req, present := storage.AuthCache().GetAndDelete(r.Form.Get("code")) 117 117 if !present { 118 - handlers.SendErrorJSON(w, http.StatusBadRequest, "invalid_request", "invalid authorization") 118 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", "invalid authorization") 119 119 return 120 120 } 121 121 authRequest := req.Value() 122 122 123 123 err := i.Server.ValidateTokenExchange(authRequest, r) 124 124 if err != nil { 125 - handlers.SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 125 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 126 126 return 127 127 } 128 128 ··· 159 159 160 160 // An actual server may want to include the "profile" in the response if the 161 161 // scope "profile" is included. 162 - handlers.SendJSON(w, http.StatusOK, response) 162 + SendJSON(w, http.StatusOK, response) 163 163 } 164 164 165 165 func (i *IndieAuth) HandleAuthApproval(w http.ResponseWriter, r *http.Request) { ··· 168 168 // whether temporary or not, cookies, etc. 169 169 req, err := i.Server.ParseAuthorization(r) 170 170 if err != nil { 171 - handlers.SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 171 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 172 172 return 173 173 } 174 174 ··· 192 192 tokenStr = strings.TrimSpace(tokenStr) 193 193 194 194 if len(tokenStr) <= 0 { 195 - handlers.SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "no credentials") 195 + SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "no credentials") 196 196 return 197 197 } 198 198 ··· 201 201 }) 202 202 203 203 if err != nil { 204 - handlers.SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "invalid token") 204 + SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "invalid token") 205 205 return 206 206 } else if claims, ok := token.Claims.(*CustomTokenClaims); ok { 207 207 ctx := context.WithValue(r.Context(), scopesContextKey, claims.Scopes) 208 208 next.ServeHTTP(w, r.WithContext(ctx)) 209 209 return 210 210 } else { 211 - handlers.SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "malformed claims") 211 + SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "malformed claims") 212 212 return 213 213 } 214 214 }) ··· 246 246 http.Error(w, "Unauthorized", http.StatusUnauthorized) 247 247 }) 248 248 } 249 + 250 + func SendJSON(w http.ResponseWriter, code int, data interface{}) { 251 + w.Header().Set("Content-Type", "application/json; charset=utf-8") 252 + w.WriteHeader(code) 253 + _ = json.NewEncoder(w).Encode(data) 254 + } 255 + 256 + func SendErrorJSON(w http.ResponseWriter, code int, err, errDescription string) { 257 + SendJSON(w, code, map[string]string{ 258 + "error": err, 259 + "error_description": errDescription, 260 + }) 261 + }
+19
storage/rel.go
··· 1 + package storage 2 + 3 + type RelEntry struct { 4 + Name string 5 + HREF string 6 + } 7 + 8 + var registry = make([]*RelEntry, 0) 9 + 10 + func AddRel(name string, href string) { 11 + registry = append(registry, &RelEntry{ 12 + Name: name, 13 + HREF: href, 14 + }) 15 + } 16 + 17 + func GetRels() []*RelEntry { 18 + return registry 19 + }