An experimental IndieWeb site built in Go.

refactor entire config and module structure

Closes #2
Closes #3
Closes #4

+1 -1
.github/workflows/fly-deploy.yml
··· 13 13 steps: 14 14 - uses: actions/checkout@v4 15 15 - uses: superfly/flyctl-actions/setup-flyctl@master 16 - - run: flyctl deploy --remote-only 16 + - run: flyctl deploy --remote-only --config ./config/fly.toml 17 17 env: 18 18 FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
+3 -10
Dockerfile config/Dockerfile
··· 1 1 # Build styles 2 - FROM node:22 AS build-styles 3 - ENV PNPM_HOME="/pnpm" 4 - ENV PATH="$PNPM_HOME:$PATH" 2 + FROM denoland/deno:1.45.5 AS build-styles 5 3 WORKDIR /app 6 4 7 5 COPY . /app 8 6 9 - RUN corepack enable pnpm 10 - RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 11 - RUN pnpm run build 7 + RUN deno run --allow-all npm:tailwindcss -o static/styles.css -c config/tailwind.config.js --minify 12 8 13 9 FROM golang:1.22-alpine AS build-server 14 10 WORKDIR /app ··· 34 30 RUN apk add bash 35 31 RUN mkdir -p /data 36 32 37 - COPY etc/litestream.yml /etc/litestream.yml 38 - COPY scripts/run.sh /scripts/run.sh 39 - 40 33 EXPOSE 80 41 34 42 - COPY etc/litestream.yml /etc/litestream.yml 35 + COPY config/litestream.yml /etc/litestream.yml 43 36 COPY scripts/run.sh /scripts/run.sh 44 37 45 38 CMD [ "/scripts/run.sh" ]
-78
db/storage.go
··· 1 - package db 2 - 3 - import ( 4 - "context" 5 - "log" 6 - "os" 7 - "path/filepath" 8 - "time" 9 - 10 - "github.com/puregarlic/space/models" 11 - 12 - "github.com/jellydator/ttlcache/v3" 13 - "go.hacdias.com/indielib/indieauth" 14 - 15 - "github.com/glebarez/sqlite" 16 - "gorm.io/gorm" 17 - 18 - "github.com/aws/aws-sdk-go-v2/aws" 19 - "github.com/aws/aws-sdk-go-v2/config" 20 - "github.com/aws/aws-sdk-go-v2/service/s3" 21 - ) 22 - 23 - type Storage struct { 24 - Db *gorm.DB 25 - Authorization *ttlcache.Cache[string, *indieauth.AuthenticationRequest] 26 - Media *s3.Client 27 - } 28 - 29 - type CollectionName string 30 - 31 - var ( 32 - PostCollection CollectionName = "posts" 33 - ) 34 - 35 - func NewStorage() *Storage { 36 - dataDir := filepath.Join(".", "data") 37 - if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { 38 - log.Fatal(err) 39 - } 40 - 41 - db, err := gorm.Open(sqlite.Open(filepath.Join(dataDir, "data.db")), &gorm.Config{}) 42 - if err != nil { 43 - log.Fatal(err) 44 - } 45 - 46 - if err := db.AutoMigrate(&models.Post{}); err != nil { 47 - panic(err) 48 - } 49 - 50 - sdkConfig, err := config.LoadDefaultConfig(context.TODO()) 51 - if err != nil { 52 - log.Printf("Couldn't load default configuration. Here's why: %v\n", err) 53 - panic(err) 54 - } 55 - 56 - svc := s3.NewFromConfig(sdkConfig, func(o *s3.Options) { 57 - o.BaseEndpoint = aws.String("https://" + os.Getenv("AWS_S3_ENDPOINT")) 58 - o.Region = os.Getenv("AWS_REGION") 59 - }) 60 - 61 - cache := ttlcache.New[string, *indieauth.AuthenticationRequest]( 62 - ttlcache.WithTTL[string, *indieauth.AuthenticationRequest](10 * time.Minute), 63 - ) 64 - 65 - go cache.Start() 66 - 67 - store := &Storage{ 68 - Db: db, 69 - Authorization: cache, 70 - Media: svc, 71 - } 72 - 73 - return store 74 - } 75 - 76 - func (db *Storage) Cleanup() { 77 - db.Authorization.Stop() 78 - }
-1
etc/litestream.yml config/litestream.yml
··· 1 1 dbs: 2 2 - path: /data/data.db 3 3 replicas: 4 - # - url: ${REPLICA_URL} 5 4 - type: s3 6 5 bucket: ${AWS_S3_BUCKET_NAME} 7 6 endpoint: ${AWS_S3_ENDPOINT}
fly.toml config/fly.toml
-4
gen.go
··· 1 - package main 2 - 3 - //go:generate templ generate 4 - //go:generate pnpm tailwindcss -o static/styles.css --minify
+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 + }
+37
handlers/media.go
··· 1 + package handlers 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "net/http" 7 + "os" 8 + "strings" 9 + 10 + "github.com/aws/aws-sdk-go-v2/aws" 11 + "github.com/aws/aws-sdk-go-v2/service/s3" 12 + "github.com/puregarlic/space/storage" 13 + ) 14 + 15 + func ServeMedia(w http.ResponseWriter, r *http.Request) { 16 + key := strings.TrimPrefix(r.URL.Path, "/") 17 + 18 + res, err := storage.S3().GetObject(r.Context(), &s3.GetObjectInput{ 19 + Bucket: aws.String(os.Getenv("AWS_S3_BUCKET_NAME")), 20 + Key: &key, 21 + }) 22 + if err != nil { 23 + fmt.Println("failed to get object", err) 24 + SendHttpError(w, http.StatusInternalServerError) 25 + return 26 + } 27 + 28 + defer res.Body.Close() 29 + 30 + w.Header().Set("Cache-Control", "604800") 31 + 32 + if _, err := io.Copy(w, res.Body); err != nil { 33 + fmt.Println("failed to send object", err) 34 + SendHttpError(w, http.StatusInternalServerError) 35 + return 36 + } 37 + }
+35
handlers/routes.go
··· 1 + package handlers 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/a-h/templ" 7 + "github.com/go-chi/chi/v5" 8 + "github.com/puregarlic/space/models" 9 + "github.com/puregarlic/space/pages" 10 + "github.com/puregarlic/space/storage" 11 + ) 12 + 13 + func ServeHomePage(w http.ResponseWriter, r *http.Request) { 14 + posts := make([]*models.Post, 0) 15 + result := storage.GORM().Limit(10).Find(&posts) 16 + if result.Error != nil { 17 + panic(result.Error) 18 + } 19 + 20 + templ.Handler(pages.Home(posts)).ServeHTTP(w, r) 21 + } 22 + 23 + func ServePostPage(w http.ResponseWriter, r *http.Request) { 24 + id := chi.URLParam(r, "slug") 25 + post := &models.Post{} 26 + 27 + result := storage.GORM().First(post, "id = ?", id) 28 + 29 + if result.RowsAffected == 0 { 30 + SendHttpError(w, http.StatusNotFound) 31 + return 32 + } 33 + 34 + templ.Handler(pages.Post(post)).ServeHTTP(w, r) 35 + }
+40 -33
indieauth.go services/indieauth.go
··· 1 - package main 1 + package services 2 2 3 3 import ( 4 4 "context" ··· 11 11 "strings" 12 12 "time" 13 13 14 + "github.com/puregarlic/space/handlers" 14 15 "github.com/puregarlic/space/pages" 16 + "github.com/puregarlic/space/storage" 15 17 16 18 "github.com/a-h/templ" 17 19 "github.com/aidarkhanov/nanoid" ··· 19 21 "go.hacdias.com/indielib/indieauth" 20 22 ) 21 23 24 + type IndieAuth struct { 25 + ProfileURL string 26 + Server *indieauth.Server 27 + } 28 + 22 29 // storeAuthorization stores the authorization request and returns a code for it. 23 30 // Something such as JWT tokens could be used in a production environment. 24 - func (s *server) storeAuthorization(req *indieauth.AuthenticationRequest) string { 31 + func (i *IndieAuth) storeAuthorization(req *indieauth.AuthenticationRequest) string { 25 32 code := nanoid.New() 26 33 27 - s.db.Authorization.Set(code, req, 0) 34 + storage.AuthCache().Set(code, req, 0) 28 35 29 36 return code 30 37 } ··· 40 47 scopesContextKey contextKey = "scopes" 41 48 ) 42 49 43 - // authorizationGetHandler handles the GET method for the authorization endpoint. 44 - func (s *server) authorizationGetHandler(w http.ResponseWriter, r *http.Request) { 50 + // HandleAuthGET handles the GET method for the authorization endpoint. 51 + func (i *IndieAuth) HandleAuthGET(w http.ResponseWriter, r *http.Request) { 45 52 // In a production server, this page would usually be protected. In order for 46 53 // the user to authorize this request, they must be authenticated. This could 47 54 // be done in different ways: username/password, passkeys, etc. 48 55 49 56 // Parse the authorization request. 50 - req, err := s.ias.ParseAuthorization(r) 57 + req, err := i.Server.ParseAuthorization(r) 51 58 if err != nil { 52 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 59 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 53 60 return 54 61 } 55 62 56 63 // Do a best effort attempt at fetching more information about the application 57 64 // that we can show to the user. Not all applications provide this sort of 58 65 // information. 59 - app, _ := s.ias.DiscoverApplicationMetadata(r.Context(), req.ClientID) 66 + app, _ := i.Server.DiscoverApplicationMetadata(r.Context(), req.ClientID) 60 67 61 68 // Here, we just display a small HTML document where the user has to press 62 69 // to authorize this request. Please note that this template contains a form ··· 65 72 templ.Handler(pages.Auth(req, app)).ServeHTTP(w, r) 66 73 } 67 74 68 - // authorizationPostHandler handles the POST method for the authorization endpoint. 69 - func (s *server) authorizationPostHandler(w http.ResponseWriter, r *http.Request) { 70 - s.authorizationCodeExchange(w, r, false) 75 + // HandleAuthPOST handles the POST method for the authorization endpoint. 76 + func (i *IndieAuth) HandleAuthPOST(w http.ResponseWriter, r *http.Request) { 77 + i.authorizationCodeExchange(w, r, false) 71 78 } 72 79 73 - // tokenHandler handles the token endpoint. In our case, we only accept the default 80 + // HandleToken handles the token endpoint. In our case, we only accept the default 74 81 // type which is exchanging an authorization code for a token. 75 - func (s *server) tokenHandler(w http.ResponseWriter, r *http.Request) { 82 + func (i *IndieAuth) HandleToken(w http.ResponseWriter, r *http.Request) { 76 83 if r.Method != http.MethodPost { 77 - httpError(w, http.StatusMethodNotAllowed) 84 + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 78 85 return 79 86 } 80 87 ··· 85 92 return 86 93 } 87 94 88 - s.authorizationCodeExchange(w, r, true) 95 + i.authorizationCodeExchange(w, r, true) 89 96 } 90 97 91 98 type tokenResponse struct { ··· 99 106 // authorizationCodeExchange handles the authorization code exchange. It is used by 100 107 // both the authorization handler to exchange the code for the user's profile URL, 101 108 // and by the token endpoint, to exchange the code by a token. 102 - func (s *server) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) { 109 + func (i *IndieAuth) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) { 103 110 if err := r.ParseForm(); err != nil { 104 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 111 + handlers.SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 105 112 return 106 113 } 107 114 108 115 // t := s.getAuthorization(r.Form.Get("code")) 109 - req, present := s.db.Authorization.GetAndDelete(r.Form.Get("code")) 116 + req, present := storage.AuthCache().GetAndDelete(r.Form.Get("code")) 110 117 if !present { 111 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", "invalid authorization") 118 + handlers.SendErrorJSON(w, http.StatusBadRequest, "invalid_request", "invalid authorization") 112 119 return 113 120 } 114 121 authRequest := req.Value() 115 122 116 - err := s.ias.ValidateTokenExchange(authRequest, r) 123 + err := i.Server.ValidateTokenExchange(authRequest, r) 117 124 if err != nil { 118 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 125 + handlers.SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 119 126 return 120 127 } 121 128 122 129 response := &tokenResponse{ 123 - Me: s.profileURL, 130 + Me: i.ProfileURL, 124 131 } 125 132 126 133 scopes := authRequest.Scopes ··· 152 159 153 160 // An actual server may want to include the "profile" in the response if the 154 161 // scope "profile" is included. 155 - serveJSON(w, http.StatusOK, response) 162 + handlers.SendJSON(w, http.StatusOK, response) 156 163 } 157 164 158 - func (s *server) authorizationAcceptHandler(w http.ResponseWriter, r *http.Request) { 165 + func (i *IndieAuth) HandleAuthApproval(w http.ResponseWriter, r *http.Request) { 159 166 // Parse authorization information. This only works because our authorization page 160 167 // includes all the required information. This can be done in other ways: database, 161 168 // whether temporary or not, cookies, etc. 162 - req, err := s.ias.ParseAuthorization(r) 169 + req, err := i.Server.ParseAuthorization(r) 163 170 if err != nil { 164 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 171 + handlers.SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 165 172 return 166 173 } 167 174 168 175 // Generate a random code and persist the information associated to that code. 169 176 // You could do this in other ways: database, or JWT tokens, or both, for example. 170 - code := s.storeAuthorization(req) 177 + code := i.storeAuthorization(req) 171 178 172 179 // Redirect to client callback. 173 180 query := url.Values{} ··· 176 183 http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound) 177 184 } 178 185 179 - // mustAuth is a middleware to ensure that the request is authorized. The way this 186 + // MustAuth is a middleware to ensure that the request is authorized. The way this 180 187 // works depends on the implementation. It then stores the scopes in the context. 181 - func (s *server) mustAuth(next http.Handler) http.Handler { 188 + func MustAuth(next http.Handler) http.Handler { 182 189 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 183 190 tokenStr := r.Header.Get("Authorization") 184 191 tokenStr = strings.TrimPrefix(tokenStr, "Bearer") 185 192 tokenStr = strings.TrimSpace(tokenStr) 186 193 187 194 if len(tokenStr) <= 0 { 188 - serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "no credentials") 195 + handlers.SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "no credentials") 189 196 return 190 197 } 191 198 ··· 194 201 }) 195 202 196 203 if err != nil { 197 - serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "invalid token") 204 + handlers.SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "invalid token") 198 205 return 199 206 } else if claims, ok := token.Claims.(*CustomTokenClaims); ok { 200 207 ctx := context.WithValue(r.Context(), scopesContextKey, claims.Scopes) 201 208 next.ServeHTTP(w, r.WithContext(ctx)) 202 209 return 203 210 } else { 204 - serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "malformed claims") 211 + handlers.SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "malformed claims") 205 212 return 206 213 } 207 214 }) 208 215 } 209 216 210 - func (s *server) mustBasicAuth(next http.Handler) http.Handler { 217 + func MustBasicAuth(next http.Handler) http.Handler { 211 218 user, ok := os.LookupEnv("ADMIN_USERNAME") 212 219 if !ok { 213 220 panic(errors.New("ADMIN_USERNAME is not set, cannot start"))
+24 -103
main.go
··· 1 1 package main 2 2 3 + //go:generate templ generate 4 + //go:generate deno run --allow-all npm:tailwindcss -o static/styles.css -c config/tailwind.config.js --minify 5 + 3 6 import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "io" 8 7 "os" 9 - "strings" 10 8 "time" 11 9 12 10 "log" 13 11 "net/http" 14 12 "strconv" 15 13 16 - "github.com/aws/aws-sdk-go-v2/aws" 17 - "github.com/aws/aws-sdk-go-v2/service/s3" 18 - "github.com/puregarlic/space/db" 19 - "github.com/puregarlic/space/models" 20 - "github.com/puregarlic/space/pages" 14 + "github.com/puregarlic/space/handlers" 15 + "github.com/puregarlic/space/services" 16 + "github.com/puregarlic/space/storage" 21 17 22 - "github.com/a-h/templ" 23 18 "github.com/go-chi/chi/v5" 24 19 "github.com/go-chi/chi/v5/middleware" 25 20 "github.com/go-chi/cors" ··· 56 51 log.Fatal(err) 57 52 } 58 53 59 - // Setup storage handlers 60 - store := db.NewStorage() 61 - defer store.Cleanup() 62 - 63 - // Create a new client. 64 - s := &server{ 65 - profileURL: profileURL, 66 - ias: indieauth.NewServer(true, nil), 67 - db: store, 68 - } 54 + defer storage.CleanupAuthCache() 69 55 70 56 r := chi.NewRouter() 71 57 ··· 73 59 r.Use(middleware.RealIP) 74 60 r.Use(middleware.Logger) 75 61 r.Use(middleware.Recoverer) 76 - 77 62 r.Use(httprate.LimitByIP(100, 1*time.Minute)) 78 63 79 64 // Static resources 80 65 r.Get("/static/*", http.StripPrefix("/static", http.FileServer(http.Dir("static"))).ServeHTTP) 81 66 82 67 // Pages 83 - r.Get("/", s.serveHomeTemplate) 84 - r.Get("/posts/{slug}", s.servePostTemplate) 85 - r.Get("/media/*", s.serveMedia) 68 + r.Get("/", handlers.ServeHomePage) 69 + r.Get("/posts/{slug}", handlers.ServePostPage) 70 + r.Get("/media/*", handlers.ServeMedia) 86 71 87 72 // IndieAuth handlers 88 73 r.Group(func(r chi.Router) { 89 - r.Post("/token", s.tokenHandler) 90 - r.Post("/authorization", s.authorizationPostHandler) 91 - r.Post("/authorization/accept", s.authorizationAcceptHandler) 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) 92 82 93 83 // User authentication portal 94 - r.With(s.mustBasicAuth).Get("/authorization", s.authorizationGetHandler) 84 + r.With(services.MustBasicAuth).Get("/authorization", ias.HandleAuthGET) 95 85 }) 96 86 97 87 // Micropub handler ··· 102 92 AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, 103 93 }, 104 94 )) 105 - r.Use(s.mustAuth) 95 + r.Use(services.MustAuth) 106 96 107 - mp := &micropubImplementation{s} 97 + mp := &services.Micropub{ 98 + ProfileURL: profileURL, 99 + } 108 100 mpHandler := micropub.NewHandler( 109 101 mp, 110 102 micropub.WithGetPostTypes(func() []micropub.PostType { ··· 119 111 }, 120 112 } 121 113 }), 122 - micropub.WithMediaEndpoint(s.profileURL+"micropub/media"), 114 + micropub.WithMediaEndpoint(profileURL+"micropub/media"), 123 115 ) 124 116 125 117 r.Get("/", mpHandler.ServeHTTP) ··· 143 135 log.Fatal(err) 144 136 } 145 137 } 146 - 147 - type server struct { 148 - profileURL string 149 - ias *indieauth.Server 150 - db *db.Storage 151 - } 152 - 153 - func (s *server) serveHomeTemplate(w http.ResponseWriter, r *http.Request) { 154 - posts := make([]*models.Post, 0) 155 - result := s.db.Db.Limit(10).Find(&posts) 156 - if result.Error != nil { 157 - panic(result.Error) 158 - } 159 - 160 - templ.Handler(pages.Home(s.profileURL, posts)).ServeHTTP(w, r) 161 - } 162 - 163 - func (s *server) servePostTemplate(w http.ResponseWriter, r *http.Request) { 164 - id := chi.URLParam(r, "slug") 165 - post := &models.Post{} 166 - 167 - result := s.db.Db.First(post, "id = ?", id) 168 - 169 - if result.RowsAffected == 0 { 170 - httpError(w, http.StatusNotFound) 171 - return 172 - } 173 - 174 - templ.Handler(pages.Post(post)).ServeHTTP(w, r) 175 - } 176 - 177 - func (s *server) serveMedia(w http.ResponseWriter, r *http.Request) { 178 - key := strings.TrimPrefix(r.URL.Path, "/") 179 - 180 - res, err := s.db.Media.GetObject(context.TODO(), &s3.GetObjectInput{ 181 - Bucket: aws.String(os.Getenv("AWS_S3_BUCKET_NAME")), 182 - Key: &key, 183 - }) 184 - if err != nil { 185 - fmt.Println("failed to get object", err) 186 - httpError(w, http.StatusInternalServerError) 187 - return 188 - } 189 - 190 - defer res.Body.Close() 191 - 192 - w.Header().Set("Cache-Control", "604800") 193 - 194 - if _, err := io.Copy(w, res.Body); err != nil { 195 - fmt.Println("failed to send object", err) 196 - httpError(w, http.StatusInternalServerError) 197 - return 198 - } 199 - } 200 - 201 - func httpError(w http.ResponseWriter, status int) { 202 - http.Error(w, http.StatusText(status), status) 203 - } 204 - 205 - func serveJSON(w http.ResponseWriter, code int, data interface{}) { 206 - w.Header().Set("Content-Type", "application/json; charset=utf-8") 207 - w.WriteHeader(code) 208 - _ = json.NewEncoder(w).Encode(data) 209 - } 210 - 211 - func serveErrorJSON(w http.ResponseWriter, code int, err, errDescription string) { 212 - serveJSON(w, code, map[string]string{ 213 - "error": err, 214 - "error_description": errDescription, 215 - }) 216 - }
+22 -21
micropub.go services/micropub.go
··· 1 - package main 1 + package services 2 2 3 3 import ( 4 4 "context" ··· 13 13 "strings" 14 14 15 15 "github.com/puregarlic/space/models" 16 + "github.com/puregarlic/space/storage" 16 17 17 18 "github.com/aidarkhanov/nanoid" 18 19 "github.com/aws/aws-sdk-go-v2/aws" ··· 23 24 "go.hacdias.com/indielib/micropub" 24 25 ) 25 26 26 - type micropubImplementation struct { 27 - *server 27 + type Micropub struct { 28 + ProfileURL string 28 29 } 29 30 30 31 func postIdFromUrlPath(path string) string { 31 32 return strings.TrimPrefix(path, "/posts/") 32 33 } 33 34 34 - func (s *micropubImplementation) HasScope(r *http.Request, scope string) bool { 35 + func (m *Micropub) HasScope(r *http.Request, scope string) bool { 35 36 v := r.Context().Value(scopesContextKey) 36 37 if scopes, ok := v.([]string); ok { 37 38 for _, sc := range scopes { ··· 44 45 return false 45 46 } 46 47 47 - func (s *micropubImplementation) Source(urlStr string) (map[string]any, error) { 48 + func (m *Micropub) Source(urlStr string) (map[string]any, error) { 48 49 url, err := urlpkg.Parse(urlStr) 49 50 if err != nil { 50 51 return nil, fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) ··· 53 54 id := postIdFromUrlPath(url.Path) 54 55 post := &models.Post{} 55 56 56 - res := s.server.db.Db.Find(post, "id = ?", id) 57 + res := storage.GORM().Find(post, "id = ?", id) 57 58 if res.Error != nil { 58 59 panic(res.Error) 59 60 } else if res.RowsAffected == 0 { ··· 66 67 }, nil 67 68 } 68 69 69 - func (s *micropubImplementation) SourceMany(limit, offset int) ([]map[string]any, error) { 70 + func (m *Micropub) SourceMany(limit, offset int) ([]map[string]any, error) { 70 71 return nil, micropub.ErrNotImplemented 71 72 } 72 73 73 - func (s *micropubImplementation) HandleMediaUpload(file multipart.File, header *multipart.FileHeader) (string, error) { 74 + func (m *Micropub) HandleMediaUpload(file multipart.File, header *multipart.FileHeader) (string, error) { 74 75 defer file.Close() 75 76 76 77 kind, err := filetype.MatchReader(file) ··· 83 84 } 84 85 85 86 key := fmt.Sprintf("media/%s.%s", nanoid.New(), kind.Extension) 86 - _, err = s.db.Media.PutObject(context.TODO(), &s3.PutObjectInput{ 87 + _, err = storage.S3().PutObject(context.TODO(), &s3.PutObjectInput{ 87 88 Bucket: aws.String(os.Getenv("AWS_S3_BUCKET_NAME")), 88 89 Key: &key, 89 90 Body: file, ··· 92 93 return "", fmt.Errorf("%w: %w", errors.New("failed to upload"), err) 93 94 } 94 95 95 - return s.profileURL + key, nil 96 + return m.ProfileURL + key, nil 96 97 } 97 98 98 - func (s *micropubImplementation) Create(req *micropub.Request) (string, error) { 99 + func (m *Micropub) Create(req *micropub.Request) (string, error) { 99 100 props, err := json.Marshal(req.Properties) 100 101 if err != nil { 101 102 return "", err ··· 107 108 Properties: props, 108 109 } 109 110 110 - res := s.server.db.Db.Create(post) 111 + res := storage.GORM().Create(post) 111 112 if res.Error != nil { 112 113 return "", res.Error 113 114 } 114 115 115 - return s.profileURL + "posts/" + post.ID.String(), nil 116 + return m.ProfileURL + "posts/" + post.ID.String(), nil 116 117 } 117 118 118 - func (s *micropubImplementation) Update(req *micropub.Request) (string, error) { 119 + func (m *Micropub) Update(req *micropub.Request) (string, error) { 119 120 url, err := urlpkg.Parse(req.URL) 120 121 if err != nil { 121 122 return "", fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) ··· 124 125 id := postIdFromUrlPath(url.Path) 125 126 post := &models.Post{} 126 127 127 - res := s.server.db.Db.Find(post, "id = ?", id) 128 + res := storage.GORM().Find(post, "id = ?", id) 128 129 if res.Error != nil { 129 130 panic(res.Error) 130 131 } else if res.RowsAffected != 1 { ··· 138 139 139 140 post.Properties = newProps 140 141 141 - s.server.db.Db.Save(post) 142 + storage.GORM().Save(post) 142 143 143 - return s.profileURL + url.Path, nil 144 + return url.String(), nil 144 145 } 145 146 146 - func (s *micropubImplementation) Delete(urlStr string) error { 147 + func (m *Micropub) Delete(urlStr string) error { 147 148 url, err := urlpkg.Parse(urlStr) 148 149 if err != nil { 149 150 return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) ··· 151 152 152 153 id := postIdFromUrlPath(url.Path) 153 154 154 - res := s.server.db.Db.Delete(&models.Post{}, "id = ?", id) 155 + res := storage.GORM().Delete(&models.Post{}, "id = ?", id) 155 156 if res.Error != nil { 156 157 panic(res.Error) 157 158 } else if res.RowsAffected == 0 { ··· 161 162 return nil 162 163 } 163 164 164 - func (s *micropubImplementation) Undelete(urlStr string) error { 165 + func (m *Micropub) Undelete(urlStr string) error { 165 166 url, err := urlpkg.Parse(urlStr) 166 167 if err != nil { 167 168 return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 168 169 } 169 170 170 171 id := postIdFromUrlPath(url.Path) 171 - res := s.server.db.Db.Unscoped().Model(&models.Post{}).Where("id = ?", id).Update("deleted_at", nil) 172 + res := storage.GORM().Unscoped().Model(&models.Post{}).Where("id = ?", id).Update("deleted_at", nil) 172 173 if res.Error != nil { 173 174 return res.Error 174 175 } else if res.RowsAffected != 1 {
-15
package.json
··· 1 - { 2 - "name": "puregarlicspace", 3 - "version": "1.0.0", 4 - "description": "", 5 - "main": "index.js", 6 - "scripts": { 7 - "build": "tailwindcss -o static/styles.css --minify" 8 - }, 9 - "keywords": [], 10 - "author": "", 11 - "license": "ISC", 12 - "devDependencies": { 13 - "tailwindcss": "^3.4.7" 14 - } 15 - }
+1 -3
pages/home.templ
··· 13 13 return props[name] 14 14 } 15 15 16 - templ Home(profileUrl string, posts []*models.Post) { 16 + templ Home(posts []*models.Post) { 17 17 <!DOCTYPE html> 18 18 <html> 19 19 <head> ··· 26 26 </head> 27 27 <body> 28 28 <h1>Micropub and IndieAuth Server Demo</h1> 29 - 30 - <p>Sign in on a website that supports IndieAuth. Use <code>{ profileUrl }</code> as your domain.</p> 31 29 32 30 <h2>Posts</h2> 33 31
+12 -25
pages/home_templ.go
··· 21 21 return props[name] 22 22 } 23 23 24 - func Home(profileUrl string, posts []*models.Post) templ.Component { 24 + func Home(posts []*models.Post) templ.Component { 25 25 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 26 26 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 27 27 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) ··· 39 39 templ_7745c5c3_Var1 = templ.NopComponent 40 40 } 41 41 ctx = templ.ClearChildren(ctx) 42 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><title>Micropub and IndieAuth Server Demo</title><link rel=\"authorization_endpoint\" href=\"/authorization\"><link rel=\"token_endpoint\" href=\"/token\"><link rel=\"micropub\" href=\"/micropub\"></head><body><h1>Micropub and IndieAuth Server Demo</h1><p>Sign in on a website that supports IndieAuth. Use <code>") 43 - if templ_7745c5c3_Err != nil { 44 - return templ_7745c5c3_Err 45 - } 46 - var templ_7745c5c3_Var2 string 47 - templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(profileUrl) 48 - if templ_7745c5c3_Err != nil { 49 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/home.templ`, Line: 30, Col: 77} 50 - } 51 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 52 - if templ_7745c5c3_Err != nil { 53 - return templ_7745c5c3_Err 54 - } 55 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</code> as your domain.</p><h2>Posts</h2><p>You can create posts using a Micropub client.</p><ul>") 42 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><title>Micropub and IndieAuth Server Demo</title><link rel=\"authorization_endpoint\" href=\"/authorization\"><link rel=\"token_endpoint\" href=\"/token\"><link rel=\"micropub\" href=\"/micropub\"></head><body><h1>Micropub and IndieAuth Server Demo</h1><h2>Posts</h2><p>You can create posts using a Micropub client.</p><ul>") 56 43 if templ_7745c5c3_Err != nil { 57 44 return templ_7745c5c3_Err 58 45 } ··· 61 48 if templ_7745c5c3_Err != nil { 62 49 return templ_7745c5c3_Err 63 50 } 64 - var templ_7745c5c3_Var3 templ.SafeURL = templ.URL("/posts/" + post.ID.String()) 65 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3))) 51 + var templ_7745c5c3_Var2 templ.SafeURL = templ.URL("/posts/" + post.ID.String()) 52 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2))) 66 53 if templ_7745c5c3_Err != nil { 67 54 return templ_7745c5c3_Err 68 55 } ··· 70 57 if templ_7745c5c3_Err != nil { 71 58 return templ_7745c5c3_Err 72 59 } 73 - var templ_7745c5c3_Var4 string 74 - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(post.ID.String()) 60 + var templ_7745c5c3_Var3 string 61 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(post.ID.String()) 75 62 if templ_7745c5c3_Err != nil { 76 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/home.templ`, Line: 37, Col: 82} 63 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/home.templ`, Line: 35, Col: 82} 77 64 } 78 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 65 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 79 66 if templ_7745c5c3_Err != nil { 80 67 return templ_7745c5c3_Err 81 68 } ··· 83 70 if templ_7745c5c3_Err != nil { 84 71 return templ_7745c5c3_Err 85 72 } 86 - var templ_7745c5c3_Var5 string 87 - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(getPostPropertyValue(post, "content"))) 73 + var templ_7745c5c3_Var4 string 74 + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(getPostPropertyValue(post, "content"))) 88 75 if templ_7745c5c3_Err != nil { 89 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/home.templ`, Line: 37, Col: 142} 76 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/home.templ`, Line: 35, Col: 142} 90 77 } 91 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 78 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 92 79 if templ_7745c5c3_Err != nil { 93 80 return templ_7745c5c3_Err 94 81 }
-842
pnpm-lock.yaml
··· 1 - lockfileVersion: '9.0' 2 - 3 - settings: 4 - autoInstallPeers: true 5 - excludeLinksFromLockfile: false 6 - 7 - importers: 8 - 9 - .: 10 - devDependencies: 11 - tailwindcss: 12 - specifier: ^3.4.7 13 - version: 3.4.7 14 - 15 - packages: 16 - 17 - '@alloc/quick-lru@5.2.0': 18 - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 19 - engines: {node: '>=10'} 20 - 21 - '@isaacs/cliui@8.0.2': 22 - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 23 - engines: {node: '>=12'} 24 - 25 - '@jridgewell/gen-mapping@0.3.5': 26 - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 27 - engines: {node: '>=6.0.0'} 28 - 29 - '@jridgewell/resolve-uri@3.1.2': 30 - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 31 - engines: {node: '>=6.0.0'} 32 - 33 - '@jridgewell/set-array@1.2.1': 34 - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 35 - engines: {node: '>=6.0.0'} 36 - 37 - '@jridgewell/sourcemap-codec@1.5.0': 38 - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 39 - 40 - '@jridgewell/trace-mapping@0.3.25': 41 - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 42 - 43 - '@nodelib/fs.scandir@2.1.5': 44 - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 45 - engines: {node: '>= 8'} 46 - 47 - '@nodelib/fs.stat@2.0.5': 48 - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 49 - engines: {node: '>= 8'} 50 - 51 - '@nodelib/fs.walk@1.2.8': 52 - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 53 - engines: {node: '>= 8'} 54 - 55 - '@pkgjs/parseargs@0.11.0': 56 - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 57 - engines: {node: '>=14'} 58 - 59 - ansi-regex@5.0.1: 60 - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 61 - engines: {node: '>=8'} 62 - 63 - ansi-regex@6.0.1: 64 - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} 65 - engines: {node: '>=12'} 66 - 67 - ansi-styles@4.3.0: 68 - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 69 - engines: {node: '>=8'} 70 - 71 - ansi-styles@6.2.1: 72 - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 73 - engines: {node: '>=12'} 74 - 75 - any-promise@1.3.0: 76 - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 77 - 78 - anymatch@3.1.3: 79 - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 80 - engines: {node: '>= 8'} 81 - 82 - arg@5.0.2: 83 - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 84 - 85 - balanced-match@1.0.2: 86 - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 87 - 88 - binary-extensions@2.3.0: 89 - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 90 - engines: {node: '>=8'} 91 - 92 - brace-expansion@2.0.1: 93 - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 94 - 95 - braces@3.0.3: 96 - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 97 - engines: {node: '>=8'} 98 - 99 - camelcase-css@2.0.1: 100 - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 101 - engines: {node: '>= 6'} 102 - 103 - chokidar@3.6.0: 104 - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 105 - engines: {node: '>= 8.10.0'} 106 - 107 - color-convert@2.0.1: 108 - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 109 - engines: {node: '>=7.0.0'} 110 - 111 - color-name@1.1.4: 112 - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 113 - 114 - commander@4.1.1: 115 - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 116 - engines: {node: '>= 6'} 117 - 118 - cross-spawn@7.0.3: 119 - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 120 - engines: {node: '>= 8'} 121 - 122 - cssesc@3.0.0: 123 - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 124 - engines: {node: '>=4'} 125 - hasBin: true 126 - 127 - didyoumean@1.2.2: 128 - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 129 - 130 - dlv@1.1.3: 131 - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 132 - 133 - eastasianwidth@0.2.0: 134 - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 135 - 136 - emoji-regex@8.0.0: 137 - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 138 - 139 - emoji-regex@9.2.2: 140 - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 141 - 142 - fast-glob@3.3.2: 143 - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 144 - engines: {node: '>=8.6.0'} 145 - 146 - fastq@1.17.1: 147 - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 148 - 149 - fill-range@7.1.1: 150 - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 151 - engines: {node: '>=8'} 152 - 153 - foreground-child@3.2.1: 154 - resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} 155 - engines: {node: '>=14'} 156 - 157 - fsevents@2.3.3: 158 - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 159 - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 160 - os: [darwin] 161 - 162 - function-bind@1.1.2: 163 - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 164 - 165 - glob-parent@5.1.2: 166 - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 167 - engines: {node: '>= 6'} 168 - 169 - glob-parent@6.0.2: 170 - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 171 - engines: {node: '>=10.13.0'} 172 - 173 - glob@10.4.5: 174 - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 175 - hasBin: true 176 - 177 - hasown@2.0.2: 178 - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 179 - engines: {node: '>= 0.4'} 180 - 181 - is-binary-path@2.1.0: 182 - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 183 - engines: {node: '>=8'} 184 - 185 - is-core-module@2.15.0: 186 - resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==} 187 - engines: {node: '>= 0.4'} 188 - 189 - is-extglob@2.1.1: 190 - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 191 - engines: {node: '>=0.10.0'} 192 - 193 - is-fullwidth-code-point@3.0.0: 194 - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 195 - engines: {node: '>=8'} 196 - 197 - is-glob@4.0.3: 198 - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 199 - engines: {node: '>=0.10.0'} 200 - 201 - is-number@7.0.0: 202 - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 203 - engines: {node: '>=0.12.0'} 204 - 205 - isexe@2.0.0: 206 - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 207 - 208 - jackspeak@3.4.3: 209 - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 210 - 211 - jiti@1.21.6: 212 - resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} 213 - hasBin: true 214 - 215 - lilconfig@2.1.0: 216 - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} 217 - engines: {node: '>=10'} 218 - 219 - lilconfig@3.1.2: 220 - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} 221 - engines: {node: '>=14'} 222 - 223 - lines-and-columns@1.2.4: 224 - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 225 - 226 - lru-cache@10.4.3: 227 - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 228 - 229 - merge2@1.4.1: 230 - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 231 - engines: {node: '>= 8'} 232 - 233 - micromatch@4.0.7: 234 - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} 235 - engines: {node: '>=8.6'} 236 - 237 - minimatch@9.0.5: 238 - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 239 - engines: {node: '>=16 || 14 >=14.17'} 240 - 241 - minipass@7.1.2: 242 - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 243 - engines: {node: '>=16 || 14 >=14.17'} 244 - 245 - mz@2.7.0: 246 - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 247 - 248 - nanoid@3.3.7: 249 - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 250 - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 251 - hasBin: true 252 - 253 - normalize-path@3.0.0: 254 - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 255 - engines: {node: '>=0.10.0'} 256 - 257 - object-assign@4.1.1: 258 - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 259 - engines: {node: '>=0.10.0'} 260 - 261 - object-hash@3.0.0: 262 - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 263 - engines: {node: '>= 6'} 264 - 265 - package-json-from-dist@1.0.0: 266 - resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} 267 - 268 - path-key@3.1.1: 269 - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 270 - engines: {node: '>=8'} 271 - 272 - path-parse@1.0.7: 273 - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 274 - 275 - path-scurry@1.11.1: 276 - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 277 - engines: {node: '>=16 || 14 >=14.18'} 278 - 279 - picocolors@1.0.1: 280 - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} 281 - 282 - picomatch@2.3.1: 283 - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 284 - engines: {node: '>=8.6'} 285 - 286 - pify@2.3.0: 287 - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 288 - engines: {node: '>=0.10.0'} 289 - 290 - pirates@4.0.6: 291 - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 292 - engines: {node: '>= 6'} 293 - 294 - postcss-import@15.1.0: 295 - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 296 - engines: {node: '>=14.0.0'} 297 - peerDependencies: 298 - postcss: ^8.0.0 299 - 300 - postcss-js@4.0.1: 301 - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 302 - engines: {node: ^12 || ^14 || >= 16} 303 - peerDependencies: 304 - postcss: ^8.4.21 305 - 306 - postcss-load-config@4.0.2: 307 - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} 308 - engines: {node: '>= 14'} 309 - peerDependencies: 310 - postcss: '>=8.0.9' 311 - ts-node: '>=9.0.0' 312 - peerDependenciesMeta: 313 - postcss: 314 - optional: true 315 - ts-node: 316 - optional: true 317 - 318 - postcss-nested@6.2.0: 319 - resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} 320 - engines: {node: '>=12.0'} 321 - peerDependencies: 322 - postcss: ^8.2.14 323 - 324 - postcss-selector-parser@6.1.1: 325 - resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==} 326 - engines: {node: '>=4'} 327 - 328 - postcss-value-parser@4.2.0: 329 - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 330 - 331 - postcss@8.4.40: 332 - resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} 333 - engines: {node: ^10 || ^12 || >=14} 334 - 335 - queue-microtask@1.2.3: 336 - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 337 - 338 - read-cache@1.0.0: 339 - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 340 - 341 - readdirp@3.6.0: 342 - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 343 - engines: {node: '>=8.10.0'} 344 - 345 - resolve@1.22.8: 346 - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 347 - hasBin: true 348 - 349 - reusify@1.0.4: 350 - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 351 - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 352 - 353 - run-parallel@1.2.0: 354 - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 355 - 356 - shebang-command@2.0.0: 357 - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 358 - engines: {node: '>=8'} 359 - 360 - shebang-regex@3.0.0: 361 - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 362 - engines: {node: '>=8'} 363 - 364 - signal-exit@4.1.0: 365 - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 366 - engines: {node: '>=14'} 367 - 368 - source-map-js@1.2.0: 369 - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 370 - engines: {node: '>=0.10.0'} 371 - 372 - string-width@4.2.3: 373 - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 374 - engines: {node: '>=8'} 375 - 376 - string-width@5.1.2: 377 - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 378 - engines: {node: '>=12'} 379 - 380 - strip-ansi@6.0.1: 381 - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 382 - engines: {node: '>=8'} 383 - 384 - strip-ansi@7.1.0: 385 - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 386 - engines: {node: '>=12'} 387 - 388 - sucrase@3.35.0: 389 - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 390 - engines: {node: '>=16 || 14 >=14.17'} 391 - hasBin: true 392 - 393 - supports-preserve-symlinks-flag@1.0.0: 394 - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 395 - engines: {node: '>= 0.4'} 396 - 397 - tailwindcss@3.4.7: 398 - resolution: {integrity: sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==} 399 - engines: {node: '>=14.0.0'} 400 - hasBin: true 401 - 402 - thenify-all@1.6.0: 403 - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 404 - engines: {node: '>=0.8'} 405 - 406 - thenify@3.3.1: 407 - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 408 - 409 - to-regex-range@5.0.1: 410 - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 411 - engines: {node: '>=8.0'} 412 - 413 - ts-interface-checker@0.1.13: 414 - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 415 - 416 - util-deprecate@1.0.2: 417 - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 418 - 419 - which@2.0.2: 420 - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 421 - engines: {node: '>= 8'} 422 - hasBin: true 423 - 424 - wrap-ansi@7.0.0: 425 - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 426 - engines: {node: '>=10'} 427 - 428 - wrap-ansi@8.1.0: 429 - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 430 - engines: {node: '>=12'} 431 - 432 - yaml@2.5.0: 433 - resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} 434 - engines: {node: '>= 14'} 435 - hasBin: true 436 - 437 - snapshots: 438 - 439 - '@alloc/quick-lru@5.2.0': {} 440 - 441 - '@isaacs/cliui@8.0.2': 442 - dependencies: 443 - string-width: 5.1.2 444 - string-width-cjs: string-width@4.2.3 445 - strip-ansi: 7.1.0 446 - strip-ansi-cjs: strip-ansi@6.0.1 447 - wrap-ansi: 8.1.0 448 - wrap-ansi-cjs: wrap-ansi@7.0.0 449 - 450 - '@jridgewell/gen-mapping@0.3.5': 451 - dependencies: 452 - '@jridgewell/set-array': 1.2.1 453 - '@jridgewell/sourcemap-codec': 1.5.0 454 - '@jridgewell/trace-mapping': 0.3.25 455 - 456 - '@jridgewell/resolve-uri@3.1.2': {} 457 - 458 - '@jridgewell/set-array@1.2.1': {} 459 - 460 - '@jridgewell/sourcemap-codec@1.5.0': {} 461 - 462 - '@jridgewell/trace-mapping@0.3.25': 463 - dependencies: 464 - '@jridgewell/resolve-uri': 3.1.2 465 - '@jridgewell/sourcemap-codec': 1.5.0 466 - 467 - '@nodelib/fs.scandir@2.1.5': 468 - dependencies: 469 - '@nodelib/fs.stat': 2.0.5 470 - run-parallel: 1.2.0 471 - 472 - '@nodelib/fs.stat@2.0.5': {} 473 - 474 - '@nodelib/fs.walk@1.2.8': 475 - dependencies: 476 - '@nodelib/fs.scandir': 2.1.5 477 - fastq: 1.17.1 478 - 479 - '@pkgjs/parseargs@0.11.0': 480 - optional: true 481 - 482 - ansi-regex@5.0.1: {} 483 - 484 - ansi-regex@6.0.1: {} 485 - 486 - ansi-styles@4.3.0: 487 - dependencies: 488 - color-convert: 2.0.1 489 - 490 - ansi-styles@6.2.1: {} 491 - 492 - any-promise@1.3.0: {} 493 - 494 - anymatch@3.1.3: 495 - dependencies: 496 - normalize-path: 3.0.0 497 - picomatch: 2.3.1 498 - 499 - arg@5.0.2: {} 500 - 501 - balanced-match@1.0.2: {} 502 - 503 - binary-extensions@2.3.0: {} 504 - 505 - brace-expansion@2.0.1: 506 - dependencies: 507 - balanced-match: 1.0.2 508 - 509 - braces@3.0.3: 510 - dependencies: 511 - fill-range: 7.1.1 512 - 513 - camelcase-css@2.0.1: {} 514 - 515 - chokidar@3.6.0: 516 - dependencies: 517 - anymatch: 3.1.3 518 - braces: 3.0.3 519 - glob-parent: 5.1.2 520 - is-binary-path: 2.1.0 521 - is-glob: 4.0.3 522 - normalize-path: 3.0.0 523 - readdirp: 3.6.0 524 - optionalDependencies: 525 - fsevents: 2.3.3 526 - 527 - color-convert@2.0.1: 528 - dependencies: 529 - color-name: 1.1.4 530 - 531 - color-name@1.1.4: {} 532 - 533 - commander@4.1.1: {} 534 - 535 - cross-spawn@7.0.3: 536 - dependencies: 537 - path-key: 3.1.1 538 - shebang-command: 2.0.0 539 - which: 2.0.2 540 - 541 - cssesc@3.0.0: {} 542 - 543 - didyoumean@1.2.2: {} 544 - 545 - dlv@1.1.3: {} 546 - 547 - eastasianwidth@0.2.0: {} 548 - 549 - emoji-regex@8.0.0: {} 550 - 551 - emoji-regex@9.2.2: {} 552 - 553 - fast-glob@3.3.2: 554 - dependencies: 555 - '@nodelib/fs.stat': 2.0.5 556 - '@nodelib/fs.walk': 1.2.8 557 - glob-parent: 5.1.2 558 - merge2: 1.4.1 559 - micromatch: 4.0.7 560 - 561 - fastq@1.17.1: 562 - dependencies: 563 - reusify: 1.0.4 564 - 565 - fill-range@7.1.1: 566 - dependencies: 567 - to-regex-range: 5.0.1 568 - 569 - foreground-child@3.2.1: 570 - dependencies: 571 - cross-spawn: 7.0.3 572 - signal-exit: 4.1.0 573 - 574 - fsevents@2.3.3: 575 - optional: true 576 - 577 - function-bind@1.1.2: {} 578 - 579 - glob-parent@5.1.2: 580 - dependencies: 581 - is-glob: 4.0.3 582 - 583 - glob-parent@6.0.2: 584 - dependencies: 585 - is-glob: 4.0.3 586 - 587 - glob@10.4.5: 588 - dependencies: 589 - foreground-child: 3.2.1 590 - jackspeak: 3.4.3 591 - minimatch: 9.0.5 592 - minipass: 7.1.2 593 - package-json-from-dist: 1.0.0 594 - path-scurry: 1.11.1 595 - 596 - hasown@2.0.2: 597 - dependencies: 598 - function-bind: 1.1.2 599 - 600 - is-binary-path@2.1.0: 601 - dependencies: 602 - binary-extensions: 2.3.0 603 - 604 - is-core-module@2.15.0: 605 - dependencies: 606 - hasown: 2.0.2 607 - 608 - is-extglob@2.1.1: {} 609 - 610 - is-fullwidth-code-point@3.0.0: {} 611 - 612 - is-glob@4.0.3: 613 - dependencies: 614 - is-extglob: 2.1.1 615 - 616 - is-number@7.0.0: {} 617 - 618 - isexe@2.0.0: {} 619 - 620 - jackspeak@3.4.3: 621 - dependencies: 622 - '@isaacs/cliui': 8.0.2 623 - optionalDependencies: 624 - '@pkgjs/parseargs': 0.11.0 625 - 626 - jiti@1.21.6: {} 627 - 628 - lilconfig@2.1.0: {} 629 - 630 - lilconfig@3.1.2: {} 631 - 632 - lines-and-columns@1.2.4: {} 633 - 634 - lru-cache@10.4.3: {} 635 - 636 - merge2@1.4.1: {} 637 - 638 - micromatch@4.0.7: 639 - dependencies: 640 - braces: 3.0.3 641 - picomatch: 2.3.1 642 - 643 - minimatch@9.0.5: 644 - dependencies: 645 - brace-expansion: 2.0.1 646 - 647 - minipass@7.1.2: {} 648 - 649 - mz@2.7.0: 650 - dependencies: 651 - any-promise: 1.3.0 652 - object-assign: 4.1.1 653 - thenify-all: 1.6.0 654 - 655 - nanoid@3.3.7: {} 656 - 657 - normalize-path@3.0.0: {} 658 - 659 - object-assign@4.1.1: {} 660 - 661 - object-hash@3.0.0: {} 662 - 663 - package-json-from-dist@1.0.0: {} 664 - 665 - path-key@3.1.1: {} 666 - 667 - path-parse@1.0.7: {} 668 - 669 - path-scurry@1.11.1: 670 - dependencies: 671 - lru-cache: 10.4.3 672 - minipass: 7.1.2 673 - 674 - picocolors@1.0.1: {} 675 - 676 - picomatch@2.3.1: {} 677 - 678 - pify@2.3.0: {} 679 - 680 - pirates@4.0.6: {} 681 - 682 - postcss-import@15.1.0(postcss@8.4.40): 683 - dependencies: 684 - postcss: 8.4.40 685 - postcss-value-parser: 4.2.0 686 - read-cache: 1.0.0 687 - resolve: 1.22.8 688 - 689 - postcss-js@4.0.1(postcss@8.4.40): 690 - dependencies: 691 - camelcase-css: 2.0.1 692 - postcss: 8.4.40 693 - 694 - postcss-load-config@4.0.2(postcss@8.4.40): 695 - dependencies: 696 - lilconfig: 3.1.2 697 - yaml: 2.5.0 698 - optionalDependencies: 699 - postcss: 8.4.40 700 - 701 - postcss-nested@6.2.0(postcss@8.4.40): 702 - dependencies: 703 - postcss: 8.4.40 704 - postcss-selector-parser: 6.1.1 705 - 706 - postcss-selector-parser@6.1.1: 707 - dependencies: 708 - cssesc: 3.0.0 709 - util-deprecate: 1.0.2 710 - 711 - postcss-value-parser@4.2.0: {} 712 - 713 - postcss@8.4.40: 714 - dependencies: 715 - nanoid: 3.3.7 716 - picocolors: 1.0.1 717 - source-map-js: 1.2.0 718 - 719 - queue-microtask@1.2.3: {} 720 - 721 - read-cache@1.0.0: 722 - dependencies: 723 - pify: 2.3.0 724 - 725 - readdirp@3.6.0: 726 - dependencies: 727 - picomatch: 2.3.1 728 - 729 - resolve@1.22.8: 730 - dependencies: 731 - is-core-module: 2.15.0 732 - path-parse: 1.0.7 733 - supports-preserve-symlinks-flag: 1.0.0 734 - 735 - reusify@1.0.4: {} 736 - 737 - run-parallel@1.2.0: 738 - dependencies: 739 - queue-microtask: 1.2.3 740 - 741 - shebang-command@2.0.0: 742 - dependencies: 743 - shebang-regex: 3.0.0 744 - 745 - shebang-regex@3.0.0: {} 746 - 747 - signal-exit@4.1.0: {} 748 - 749 - source-map-js@1.2.0: {} 750 - 751 - string-width@4.2.3: 752 - dependencies: 753 - emoji-regex: 8.0.0 754 - is-fullwidth-code-point: 3.0.0 755 - strip-ansi: 6.0.1 756 - 757 - string-width@5.1.2: 758 - dependencies: 759 - eastasianwidth: 0.2.0 760 - emoji-regex: 9.2.2 761 - strip-ansi: 7.1.0 762 - 763 - strip-ansi@6.0.1: 764 - dependencies: 765 - ansi-regex: 5.0.1 766 - 767 - strip-ansi@7.1.0: 768 - dependencies: 769 - ansi-regex: 6.0.1 770 - 771 - sucrase@3.35.0: 772 - dependencies: 773 - '@jridgewell/gen-mapping': 0.3.5 774 - commander: 4.1.1 775 - glob: 10.4.5 776 - lines-and-columns: 1.2.4 777 - mz: 2.7.0 778 - pirates: 4.0.6 779 - ts-interface-checker: 0.1.13 780 - 781 - supports-preserve-symlinks-flag@1.0.0: {} 782 - 783 - tailwindcss@3.4.7: 784 - dependencies: 785 - '@alloc/quick-lru': 5.2.0 786 - arg: 5.0.2 787 - chokidar: 3.6.0 788 - didyoumean: 1.2.2 789 - dlv: 1.1.3 790 - fast-glob: 3.3.2 791 - glob-parent: 6.0.2 792 - is-glob: 4.0.3 793 - jiti: 1.21.6 794 - lilconfig: 2.1.0 795 - micromatch: 4.0.7 796 - normalize-path: 3.0.0 797 - object-hash: 3.0.0 798 - picocolors: 1.0.1 799 - postcss: 8.4.40 800 - postcss-import: 15.1.0(postcss@8.4.40) 801 - postcss-js: 4.0.1(postcss@8.4.40) 802 - postcss-load-config: 4.0.2(postcss@8.4.40) 803 - postcss-nested: 6.2.0(postcss@8.4.40) 804 - postcss-selector-parser: 6.1.1 805 - resolve: 1.22.8 806 - sucrase: 3.35.0 807 - transitivePeerDependencies: 808 - - ts-node 809 - 810 - thenify-all@1.6.0: 811 - dependencies: 812 - thenify: 3.3.1 813 - 814 - thenify@3.3.1: 815 - dependencies: 816 - any-promise: 1.3.0 817 - 818 - to-regex-range@5.0.1: 819 - dependencies: 820 - is-number: 7.0.0 821 - 822 - ts-interface-checker@0.1.13: {} 823 - 824 - util-deprecate@1.0.2: {} 825 - 826 - which@2.0.2: 827 - dependencies: 828 - isexe: 2.0.0 829 - 830 - wrap-ansi@7.0.0: 831 - dependencies: 832 - ansi-styles: 4.3.0 833 - string-width: 4.2.3 834 - strip-ansi: 6.0.1 835 - 836 - wrap-ansi@8.1.0: 837 - dependencies: 838 - ansi-styles: 6.2.1 839 - string-width: 5.1.2 840 - strip-ansi: 7.1.0 841 - 842 - yaml@2.5.0: {}
+30
storage/cache.go
··· 1 + package storage 2 + 3 + import ( 4 + "time" 5 + 6 + "github.com/jellydator/ttlcache/v3" 7 + "go.hacdias.com/indielib/indieauth" 8 + ) 9 + 10 + var authCache *ttlcache.Cache[string, *indieauth.AuthenticationRequest] 11 + 12 + func CleanupAuthCache() { 13 + AuthCache().Stop() 14 + } 15 + 16 + func AuthCache() *ttlcache.Cache[string, *indieauth.AuthenticationRequest] { 17 + if authCache != nil { 18 + return authCache 19 + } 20 + 21 + cache := ttlcache.New( 22 + ttlcache.WithTTL[string, *indieauth.AuthenticationRequest](10 * time.Minute), 23 + ) 24 + 25 + go cache.Start() 26 + 27 + authCache = cache 28 + 29 + return cache 30 + }
+37
storage/gorm.go
··· 1 + package storage 2 + 3 + import ( 4 + "log" 5 + "os" 6 + "path/filepath" 7 + 8 + "github.com/glebarez/sqlite" 9 + "gorm.io/gorm" 10 + ) 11 + 12 + var orm *gorm.DB 13 + 14 + func GORM() *gorm.DB { 15 + if orm != nil { 16 + return orm 17 + } 18 + 19 + dataDir := filepath.Join(".", "data") 20 + if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { 21 + log.Fatal(err) 22 + } 23 + 24 + db, err := gorm.Open(sqlite.Open(filepath.Join(dataDir, "data.db")), &gorm.Config{}) 25 + if err != nil { 26 + log.Fatal(err) 27 + } 28 + 29 + orm = db 30 + 31 + // TODO: Move migration to `models` package 32 + // if err := db.AutoMigrate(&models.Post{}); err != nil { 33 + // log.Fatal(err) 34 + // } 35 + 36 + return db 37 + }
+34
storage/s3.go
··· 1 + package storage 2 + 3 + import ( 4 + "context" 5 + "log" 6 + "os" 7 + 8 + "github.com/aws/aws-sdk-go-v2/aws" 9 + "github.com/aws/aws-sdk-go-v2/config" 10 + "github.com/aws/aws-sdk-go-v2/service/s3" 11 + ) 12 + 13 + var s3Client *s3.Client 14 + 15 + func S3() *s3.Client { 16 + if s3Client != nil { 17 + return s3Client 18 + } 19 + 20 + sdkConfig, err := config.LoadDefaultConfig(context.Background()) 21 + if err != nil { 22 + log.Printf("Couldn't load default configuration. Here's why: %v\n", err) 23 + panic(err) 24 + } 25 + 26 + svc := s3.NewFromConfig(sdkConfig, func(o *s3.Options) { 27 + o.BaseEndpoint = aws.String("https://" + os.Getenv("AWS_S3_ENDPOINT")) 28 + o.Region = os.Getenv("AWS_REGION") 29 + }) 30 + 31 + s3Client = svc 32 + 33 + return svc 34 + }
tailwind.config.js config/tailwind.config.js