An experimental IndieWeb site built in Go.

Compare changes

Choose any two refs to compare.

+6
.env.example
··· 9 9 ADMIN_PASSWORD="me" 10 10 11 11 JWT_SECRET="bogus" 12 + 13 + AWS_ACCESS_KEY_ID= 14 + AWS_SECRET_ACCESS_KEY= 15 + AWS_S3_BUCKET_NAME= 16 + AWS_S3_ENDPOINT=fly.storage.tigris.dev 17 + AWS_REGION=auto
+1
.gitattributes
··· 1 + *.sh text eol=lf
+18
.github/workflows/fly-deploy.yml
··· 1 + # See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ 2 + 3 + name: Fly Deploy 4 + on: 5 + push: 6 + branches: 7 + - main 8 + jobs: 9 + deploy: 10 + name: Deploy app 11 + runs-on: ubuntu-latest 12 + concurrency: deploy-group # optional: ensure only one action runs at a time 13 + steps: 14 + - uses: actions/checkout@v4 15 + - uses: superfly/flyctl-actions/setup-flyctl@master 16 + - run: flyctl deploy --remote-only --config ./config/fly.toml 17 + env: 18 + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
+2
.gitignore
··· 3 3 node_modules 4 4 5 5 data/**/*.db 6 + tmp/ 7 + *_templ.txt 6 8 7 9 static/styles.css
-30
Dockerfile
··· 1 - # Build styles 2 - FROM node:22 AS build-styles 3 - ENV PNPM_HOME="/pnpm" 4 - ENV PATH="$PNPM_HOME:$PATH" 5 - WORKDIR /app 6 - 7 - COPY . /app 8 - 9 - RUN corepack enable pnpm 10 - RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 11 - RUN pnpm run build 12 - 13 - FROM golang:1.22-alpine AS build-server 14 - WORKDIR /app 15 - 16 - COPY . /app 17 - 18 - RUN go mod download 19 - RUN mkdir data 20 - RUN mkdir data/docs 21 - 22 - RUN CGO_ENABLED=0 GOOS=linux go build -o /space 23 - 24 - FROM scratch 25 - WORKDIR / 26 - COPY --from=build-styles /app/static /static 27 - COPY --from=build-server /app/data /data 28 - COPY --from=build-server /space /space 29 - EXPOSE 80 30 - ENTRYPOINT ["/space"]
+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.
+51
config/.air.toml
··· 1 + root = "." 2 + testdata_dir = "testdata" 3 + tmp_dir = "tmp" 4 + 5 + [build] 6 + args_bin = [] 7 + bin = "./tmp/main" 8 + cmd = "go build -o ./tmp/main ." 9 + delay = 1000 10 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] 11 + exclude_file = [] 12 + exclude_regex = ["_test.go"] 13 + exclude_unchanged = false 14 + follow_symlink = false 15 + full_bin = "" 16 + include_dir = ["html"] 17 + include_ext = ["go", "tpl", "tmpl", "html"] 18 + include_file = [] 19 + kill_delay = "0s" 20 + log = "build-errors.log" 21 + poll = false 22 + poll_interval = 0 23 + post_cmd = [] 24 + pre_cmd = [] 25 + rerun = false 26 + rerun_delay = 500 27 + send_interrupt = false 28 + stop_on_error = false 29 + 30 + [color] 31 + app = "" 32 + build = "yellow" 33 + main = "magenta" 34 + runner = "green" 35 + watcher = "cyan" 36 + 37 + [log] 38 + main_only = false 39 + time = false 40 + 41 + [misc] 42 + clean_on_exit = false 43 + 44 + [proxy] 45 + app_port = 0 46 + enabled = false 47 + proxy_port = 0 48 + 49 + [screen] 50 + clear_on_rebuild = false 51 + keep_scroll = true
+38
config/Dockerfile
··· 1 + # Build styles 2 + FROM denoland/deno:2.5.2 AS build-styles 3 + WORKDIR /app 4 + 5 + COPY . /app 6 + 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 + 9 + FROM golang:1.24-alpine AS build-server 10 + WORKDIR /app 11 + 12 + COPY . /app 13 + 14 + RUN go mod download 15 + 16 + ENV CGO_ENABLED=0 17 + ENV GOOS=linux 18 + RUN go build -ldflags '-s -w -extldflags "-static"' -tags osusergo,netgo,sqlite_omit_load_extension -o /space 19 + 20 + ADD https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz /tmp/litestream.tar.gz 21 + RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz 22 + 23 + FROM alpine 24 + WORKDIR / 25 + 26 + COPY --from=build-styles /app/static /static 27 + COPY --from=build-server /space /space 28 + COPY --from=build-server /usr/local/bin/litestream /usr/local/bin/litestream 29 + 30 + RUN apk add bash 31 + RUN mkdir -p /data 32 + 33 + EXPOSE 80 34 + 35 + COPY config/litestream.yml /etc/litestream.yml 36 + COPY scripts/run.sh /scripts/run.sh 37 + 38 + CMD [ "/scripts/run.sh" ]
+30
config/fly.toml
··· 1 + # fly.toml app configuration file generated for puregarlicspace on 2024-08-06T08:36:09-07:00 2 + # 3 + # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 + # 5 + 6 + app = 'puregarlicspace' 7 + primary_region = 'sjc' 8 + 9 + [build] 10 + dockerfile = "Dockerfile" 11 + 12 + [http_service] 13 + internal_port = 80 14 + force_https = true 15 + auto_stop_machines = 'suspend' 16 + auto_start_machines = true 17 + min_machines_running = 0 18 + processes = ['app'] 19 + 20 + [[vm]] 21 + memory = '1gb' 22 + cpu_kind = 'shared' 23 + cpus = 1 24 + 25 + [mounts] 26 + source = "puregarlicspace_data" 27 + destination = "/data" 28 + initial_size = "1GB" 29 + auto_extend_size_threshold = 80 30 + auto_extend_size_increment = "1GB"
+8
config/litestream.yml
··· 1 + dbs: 2 + - path: /data/data.db 3 + replicas: 4 + - type: s3 5 + bucket: ${AWS_S3_BUCKET_NAME} 6 + endpoint: ${AWS_S3_ENDPOINT} 7 + path: db 8 + force-path-style: true
+10
config/main.css
··· 1 + @tailwind base; 2 + @tailwind components; 3 + @tailwind utilities; 4 + 5 + @layer base { 6 + code { 7 + font-variation-settings: "MONO" 1; 8 + min-width: 0; 9 + } 10 + }
+33
config/tailwind.config.ts
··· 1 + import { Config } from "npm:tailwindcss"; 2 + 3 + export default { 4 + content: [ 5 + "./html/**/*.templ", 6 + ], 7 + theme: { 8 + extend: { 9 + fontFamily: { 10 + sans: "Recursive, sans-serif", 11 + mono: "Recursive, monospace", 12 + }, 13 + colors: { 14 + base: "#191724", 15 + surface: "#1f1d2e", 16 + overlay: "#26233a", 17 + muted: "#6e6a86", 18 + subtle: "#908caa", 19 + text: "#e0def4", 20 + love: "#eb6f92", 21 + gold: "#f6c177", 22 + rose: "#ebbcba", 23 + pine: "#31748f", 24 + foam: "#9ccfd8", 25 + iris: "#c4a7e7", 26 + highlightLow: "#21202e", 27 + highlightMed: "#403d52", 28 + highlightHigh: "#524f67", 29 + }, 30 + }, 31 + }, 32 + plugins: [], 33 + } as Config;
-61
db/storage.go
··· 1 - package db 2 - 3 - import ( 4 - "log" 5 - "os" 6 - "path/filepath" 7 - "time" 8 - 9 - "github.com/puregarlic/space/models" 10 - 11 - "github.com/jellydator/ttlcache/v3" 12 - "go.hacdias.com/indielib/indieauth" 13 - 14 - "github.com/glebarez/sqlite" 15 - "gorm.io/gorm" 16 - ) 17 - 18 - type Storage struct { 19 - Db *gorm.DB 20 - Authorization *ttlcache.Cache[string, *indieauth.AuthenticationRequest] 21 - } 22 - 23 - type CollectionName string 24 - 25 - var ( 26 - PostCollection CollectionName = "posts" 27 - ) 28 - 29 - func NewStorage() *Storage { 30 - dataDir := filepath.Join(".", "data") 31 - if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { 32 - log.Fatal(err) 33 - } 34 - 35 - db, err := gorm.Open(sqlite.Open(filepath.Join(dataDir, "data.db")), &gorm.Config{}) 36 - if err != nil { 37 - log.Fatal(err) 38 - } 39 - 40 - if err := db.AutoMigrate(&models.Post{}); err != nil { 41 - panic(err) 42 - } 43 - 44 - cache := ttlcache.New[string, *indieauth.AuthenticationRequest]( 45 - ttlcache.WithTTL[string, *indieauth.AuthenticationRequest](10 * time.Minute), 46 - ) 47 - 48 - go cache.Start() 49 - 50 - store := &Storage{ 51 - // Docs: c, 52 - Db: db, 53 - Authorization: cache, 54 - } 55 - 56 - return store 57 - } 58 - 59 - func (db *Storage) Cleanup() { 60 - db.Authorization.Stop() 61 - }
-4
gen.go
··· 1 - package main 2 - 3 - //go:generate templ generate 4 - //go:generate pnpm tailwindcss -o static/styles.css --minify
+22
go.mod
··· 5 5 require ( 6 6 codeberg.org/gruf/go-ulid v1.1.0 7 7 github.com/aidarkhanov/nanoid v1.0.8 8 + github.com/alecthomas/chroma/v2 v2.14.0 9 + github.com/aws/aws-sdk-go-v2 v1.30.3 8 10 github.com/glebarez/sqlite v1.11.0 11 + github.com/go-chi/cors v1.2.1 12 + github.com/h2non/filetype v1.1.3 9 13 github.com/jellydator/ttlcache/v3 v3.2.0 10 14 github.com/joho/godotenv v1.5.1 11 15 go.hacdias.com/indielib v0.3.1 ··· 15 19 16 20 require ( 17 21 filippo.io/edwards25519 v1.1.0 // indirect 22 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect 23 + github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect 24 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect 25 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect 26 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect 27 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 28 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect 29 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect 30 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect 31 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect 32 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect 33 + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect 34 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect 35 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect 36 + github.com/aws/smithy-go v1.20.3 // indirect 18 37 github.com/cespare/xxhash/v2 v2.3.0 // indirect 38 + github.com/dlclark/regexp2 v1.11.0 // indirect 19 39 github.com/dustin/go-humanize v1.0.1 // indirect 20 40 github.com/glebarez/go-sqlite v1.21.2 // indirect 21 41 github.com/go-sql-driver/mysql v1.8.1 // indirect ··· 36 56 37 57 require ( 38 58 github.com/a-h/templ v0.2.747 59 + github.com/aws/aws-sdk-go-v2/config v1.27.27 60 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 39 61 github.com/go-chi/chi/v5 v5.1.0 40 62 github.com/go-chi/httprate v0.12.0 41 63 github.com/golang-jwt/jwt/v5 v5.2.1
+50
go.sum
··· 6 6 github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4= 7 7 github.com/aidarkhanov/nanoid v1.0.8 h1:yxyJkgsEDFXP7+97vc6JevMcjyb03Zw+/9fqhlVXBXA= 8 8 github.com/aidarkhanov/nanoid v1.0.8/go.mod h1:vadfZHT+m4uDhttg0yY4wW3GKtl2T6i4d2Age+45pYk= 9 + github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= 10 + github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 11 + github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= 12 + github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= 13 + github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 14 + github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 9 15 github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 16 + github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= 17 + github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= 18 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= 19 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= 20 + github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= 21 + github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= 22 + github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= 23 + github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= 24 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= 25 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= 26 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= 27 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= 28 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= 29 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= 30 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 31 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 32 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= 33 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= 34 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= 35 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= 36 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= 37 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= 38 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= 39 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= 40 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= 41 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= 42 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8= 43 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE= 44 + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= 45 + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= 46 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= 47 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= 48 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= 49 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= 50 + github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= 51 + github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 10 52 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 11 53 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 12 54 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 55 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 56 + github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= 57 + github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 14 58 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 15 59 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 16 60 github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= ··· 19 63 github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= 20 64 github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= 21 65 github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 66 + github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= 67 + github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 22 68 github.com/go-chi/httprate v0.12.0 h1:08D/te3pOTJe5+VAZTQrHxwdsH2NyliiUoRD1naKaMg= 23 69 github.com/go-chi/httprate v0.12.0/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= 24 70 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= ··· 37 83 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 38 84 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 39 85 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 86 + github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= 87 + github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= 88 + github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 89 + github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 40 90 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 41 91 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 42 92 github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
+35
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 + panic(err) 25 + } 26 + 27 + defer res.Body.Close() 28 + 29 + w.Header().Set("Cache-Control", "604800") 30 + 31 + if _, err := io.Copy(w, res.Body); err != nil { 32 + fmt.Println("failed to send object", err) 33 + panic(err) 34 + } 35 + }
+36
handlers/routes.go
··· 1 + package handlers 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/go-chi/chi/v5" 7 + "github.com/puregarlic/space/models" 8 + "github.com/puregarlic/space/storage" 9 + 10 + "github.com/puregarlic/space/html/layouts" 11 + "github.com/puregarlic/space/html/pages" 12 + ) 13 + 14 + func ServeHomePage(w http.ResponseWriter, r *http.Request) { 15 + posts := make([]*models.Post, 0) 16 + result := storage.GORM().Limit(10).Order("created_at DESC").Find(&posts) 17 + if result.Error != nil { 18 + panic(result.Error) 19 + } 20 + 21 + layouts.RenderWithSidebar("", pages.Home(posts)).ServeHTTP(w, r) 22 + } 23 + 24 + func ServePostPage(w http.ResponseWriter, r *http.Request) { 25 + id := chi.URLParam(r, "slug") 26 + post := &models.Post{} 27 + 28 + result := storage.GORM().First(post, "id = ?", id) 29 + 30 + if result.RowsAffected == 0 { 31 + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 32 + return 33 + } 34 + 35 + layouts.RenderWithSidebar(string(post.MicroformatType), pages.Post(post)).ServeHTTP(w, r) 36 + }
+74
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/micropub" 13 + ) 14 + 15 + func AttachIndieAuth(r chi.Router, path string, profileUrl string) { 16 + svc := &services.IndieAuth{ 17 + ProfileURL: profileUrl, 18 + Server: indieauth.NewServer(true, nil), 19 + } 20 + 21 + r.Route(path, func(r chi.Router) { 22 + r.Post("/", svc.HandleAuthPOST) 23 + r.Post("/token", svc.HandleToken) 24 + r.Post("/accept", svc.HandleAuthApproval) 25 + 26 + // User authentication portal 27 + r.With(services.MustBasicAuth).Get("/", svc.HandleAuthGET) 28 + }) 29 + 30 + storage.AddRel("authorization_endpoint", path) 31 + storage.AddRel("token_endpoint", path+"/token") 32 + } 33 + 34 + func AttachMicropub(r chi.Router, path string, profileURL string) { 35 + mp := &services.Micropub{ 36 + ProfileURL: profileURL, 37 + } 38 + 39 + mpHandler := micropub.NewHandler( 40 + mp, 41 + micropub.WithMediaEndpoint(profileURL+"micropub/media"), 42 + ) 43 + 44 + r.Route(path, func(r chi.Router) { 45 + // Enable CORS for browser-based clients 46 + r.Use(cors.Handler( 47 + cors.Options{ 48 + AllowedHeaders: []string{ 49 + "Accept", 50 + "Authorization", 51 + "Content-Type", 52 + }, 53 + }, 54 + )) 55 + 56 + // Require access tokens for all Micropub routes 57 + r.Use(services.MustAuth) 58 + 59 + r.Get("/", mpHandler.ServeHTTP) 60 + r.Post("/", mpHandler.ServeHTTP) 61 + r.Post("/media", micropub.NewMediaHandler( 62 + mp.HandleMediaUpload, 63 + func(r *http.Request, scope string) bool { 64 + // IndieKit checks for a `media` scope, not commonly requested 65 + hasMediaScope := mp.HasScope(r, scope) 66 + hasCreateScope := mp.HasScope(r, "create") 67 + 68 + return hasMediaScope || hasCreateScope 69 + }, 70 + ).ServeHTTP) 71 + }) 72 + 73 + storage.AddRel("micropub", path) 74 + }
+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 + }
+9
html/components/posts/note.templ
··· 1 + package posts 2 + 3 + import "github.com/puregarlic/space/models" 4 + 5 + templ Note(post *models.Post) { 6 + <div class="bg-surface p-4 first:rounded-t last:rounded-b border-2 border-overlay"> 7 + { GetPostJSONProperty(post, "content")[0] } 8 + </div> 9 + }
+50
html/components/posts/note_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package posts 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/models" 12 + 13 + func Note(post *models.Post) 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("<div class=\"bg-surface p-4 first:rounded-t last:rounded-b border-2 border-overlay\">") 32 + if templ_7745c5c3_Err != nil { 33 + return templ_7745c5c3_Err 34 + } 35 + var templ_7745c5c3_Var2 string 36 + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(GetPostJSONProperty(post, "content")[0]) 37 + if templ_7745c5c3_Err != nil { 38 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/note.templ`, Line: 7, Col: 43} 39 + } 40 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 41 + if templ_7745c5c3_Err != nil { 42 + return templ_7745c5c3_Err 43 + } 44 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") 45 + if templ_7745c5c3_Err != nil { 46 + return templ_7745c5c3_Err 47 + } 48 + return templ_7745c5c3_Err 49 + }) 50 + }
+18
html/components/posts/photo.templ
··· 1 + package posts 2 + 3 + import "github.com/puregarlic/space/models" 4 + 5 + templ Photo(post *models.Post) { 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> 18 + }
+73
html/components/posts/photo_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package posts 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/models" 12 + 13 + func Photo(post *models.Post) 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("<div class=\"bg-base\">") 32 + if templ_7745c5c3_Err != nil { 33 + return templ_7745c5c3_Err 34 + } 35 + for index, photo := range GetPostJSONProperty(post, "photo") { 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=\"") 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(photo) 42 + if templ_7745c5c3_Err != nil { 43 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/photo.templ`, Line: 9, Col: 106} 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("\"><figcaption class=\"p-4 bg-surface group-last:rounded-b group-last:border-b-2 group-last:border-x-2 group-last:border-overlay\">") 50 + if templ_7745c5c3_Err != nil { 51 + return templ_7745c5c3_Err 52 + } 53 + var templ_7745c5c3_Var3 string 54 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(GetPostJSONProperty(post, "content")[index]) 55 + if templ_7745c5c3_Err != nil { 56 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/photo.templ`, Line: 13, Col: 50} 57 + } 58 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 59 + if templ_7745c5c3_Err != nil { 60 + return templ_7745c5c3_Err 61 + } 62 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</figcaption></figure>") 63 + if templ_7745c5c3_Err != nil { 64 + return templ_7745c5c3_Err 65 + } 66 + } 67 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") 68 + if templ_7745c5c3_Err != nil { 69 + return templ_7745c5c3_Err 70 + } 71 + return templ_7745c5c3_Err 72 + }) 73 + }
+92
html/components/posts/post.templ
··· 1 + package posts 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + 7 + "github.com/puregarlic/space/models" 8 + "github.com/samber/lo" 9 + "go.hacdias.com/indielib/microformats" 10 + ) 11 + 12 + var ImplementedPostTypes = []microformats.Type{ 13 + microformats.TypeNote, 14 + microformats.TypePhoto, 15 + } 16 + 17 + func GetPostJSONProperty(post *models.Post, name string) []string { 18 + var tmp map[string]any 19 + if err := json.Unmarshal(post.Properties, &tmp); err != nil { 20 + panic(err) 21 + } 22 + 23 + prop, ok := tmp[name] 24 + if !ok { 25 + return []string{""} 26 + } 27 + 28 + var out []string 29 + for _, val := range prop.([]any) { 30 + out = append(out, val.(string)) 31 + } 32 + 33 + return out 34 + } 35 + 36 + func formatPostTypeName(mfType microformats.Type) string { 37 + has := lo.ContainsBy(ImplementedPostTypes, func(postType microformats.Type) bool { 38 + return postType == mfType 39 + }) 40 + 41 + if has { 42 + return string(mfType) 43 + } else { 44 + return fmt.Sprintf("%s (oops!)", string(mfType)) 45 + } 46 + } 47 + 48 + templ PostFeedHeader(post *models.Post) { 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> 81 + } 82 + 83 + templ PostContent(post *models.Post) { 84 + switch post.MicroformatType { 85 + case microformats.TypePhoto: 86 + @Photo(post) 87 + case microformats.TypeNote: 88 + @Note(post) 89 + default: 90 + @Unsupported(post) 91 + } 92 + }
+206
html/components/posts/post_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package posts 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 + "encoding/json" 13 + "fmt" 14 + 15 + "github.com/puregarlic/space/models" 16 + "github.com/samber/lo" 17 + "go.hacdias.com/indielib/microformats" 18 + ) 19 + 20 + var ImplementedPostTypes = []microformats.Type{ 21 + microformats.TypeNote, 22 + microformats.TypePhoto, 23 + } 24 + 25 + func GetPostJSONProperty(post *models.Post, name string) []string { 26 + var tmp map[string]any 27 + if err := json.Unmarshal(post.Properties, &tmp); err != nil { 28 + panic(err) 29 + } 30 + 31 + prop, ok := tmp[name] 32 + if !ok { 33 + return []string{""} 34 + } 35 + 36 + var out []string 37 + for _, val := range prop.([]any) { 38 + out = append(out, val.(string)) 39 + } 40 + 41 + return out 42 + } 43 + 44 + func formatPostTypeName(mfType microformats.Type) string { 45 + has := lo.ContainsBy(ImplementedPostTypes, func(postType microformats.Type) bool { 46 + return postType == mfType 47 + }) 48 + 49 + if has { 50 + return string(mfType) 51 + } else { 52 + return fmt.Sprintf("%s (oops!)", string(mfType)) 53 + } 54 + } 55 + 56 + func PostFeedHeader(post *models.Post) templ.Component { 57 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 58 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 59 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 60 + if !templ_7745c5c3_IsBuffer { 61 + defer func() { 62 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 63 + if templ_7745c5c3_Err == nil { 64 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 65 + } 66 + }() 67 + } 68 + ctx = templ.InitializeContext(ctx) 69 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 70 + if templ_7745c5c3_Var1 == nil { 71 + templ_7745c5c3_Var1 = templ.NopComponent 72 + } 73 + ctx = templ.ClearChildren(ctx) 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>") 75 + if templ_7745c5c3_Err != nil { 76 + return templ_7745c5c3_Err 77 + } 78 + var templ_7745c5c3_Var2 string 79 + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(post.Timestamp()) 80 + if templ_7745c5c3_Err != nil { 81 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 50, Col: 23} 82 + } 83 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 84 + if templ_7745c5c3_Err != nil { 85 + return templ_7745c5c3_Err 86 + } 87 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><p class=\"flex gap-1.5\">") 88 + if templ_7745c5c3_Err != nil { 89 + return templ_7745c5c3_Err 90 + } 91 + var templ_7745c5c3_Var3 string 92 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(formatPostTypeName(post.MicroformatType)) 93 + if templ_7745c5c3_Err != nil { 94 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/components/posts/post.templ`, Line: 52, Col: 45} 95 + } 96 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 97 + if templ_7745c5c3_Err != nil { 98 + return templ_7745c5c3_Err 99 + } 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=\"") 101 + if templ_7745c5c3_Err != nil { 102 + return templ_7745c5c3_Err 103 + } 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))) 106 + if templ_7745c5c3_Err != nil { 107 + return templ_7745c5c3_Err 108 + } 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>") 110 + if templ_7745c5c3_Err != nil { 111 + return templ_7745c5c3_Err 112 + } 113 + return templ_7745c5c3_Err 114 + }) 115 + } 116 + 117 + func PostDetails(post *models.Post) templ.Component { 118 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 119 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 120 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 121 + if !templ_7745c5c3_IsBuffer { 122 + defer func() { 123 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 124 + if templ_7745c5c3_Err == nil { 125 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 126 + } 127 + }() 128 + } 129 + ctx = templ.InitializeContext(ctx) 130 + templ_7745c5c3_Var5 := templ.GetChildren(ctx) 131 + if templ_7745c5c3_Var5 == nil { 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 185 + } 186 + ctx = templ.ClearChildren(ctx) 187 + switch post.MicroformatType { 188 + case microformats.TypePhoto: 189 + templ_7745c5c3_Err = Photo(post).Render(ctx, templ_7745c5c3_Buffer) 190 + if templ_7745c5c3_Err != nil { 191 + return templ_7745c5c3_Err 192 + } 193 + case microformats.TypeNote: 194 + templ_7745c5c3_Err = Note(post).Render(ctx, templ_7745c5c3_Buffer) 195 + if templ_7745c5c3_Err != nil { 196 + return templ_7745c5c3_Err 197 + } 198 + default: 199 + templ_7745c5c3_Err = Unsupported(post).Render(ctx, templ_7745c5c3_Buffer) 200 + if templ_7745c5c3_Err != nil { 201 + return templ_7745c5c3_Err 202 + } 203 + } 204 + return templ_7745c5c3_Err 205 + }) 206 + }
+55
html/components/posts/unsupported.templ
··· 1 + package posts 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + 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" 11 + ) 12 + 13 + var style = styles.Get("rose-pine") 14 + var lexer = lexers.Get("json") 15 + var formatter = html.New(html.TabWidth(2), html.WithClasses(true)) 16 + 17 + var dedupeSyntaxStyles = templ.NewOnceHandle() 18 + 19 + func renderPostAsJSON(post *models.Post) string { 20 + contents, err := json.MarshalIndent(post.Properties, "", " ") 21 + if err != nil { 22 + panic(err) 23 + } 24 + 25 + iterator, err := lexer.Tokenise(nil, string(contents)) 26 + 27 + var buf bytes.Buffer 28 + formatter.Format(&buf, style, iterator) 29 + 30 + return buf.String() 31 + } 32 + 33 + func generateSyntaxClassNames() string { 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>" 40 + } 41 + 42 + templ syntaxStyleTag() { 43 + @templ.Raw(generateSyntaxClassNames()) 44 + } 45 + 46 + templ Unsupported(post *models.Post) { 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> 55 + }
+130
html/components/posts/unsupported_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package posts 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 + "bytes" 13 + "encoding/json" 14 + 15 + "github.com/alecthomas/chroma/v2/formatters/html" 16 + "github.com/alecthomas/chroma/v2/lexers" 17 + "github.com/alecthomas/chroma/v2/styles" 18 + "github.com/puregarlic/space/models" 19 + ) 20 + 21 + var style = styles.Get("rose-pine") 22 + var lexer = lexers.Get("json") 23 + var formatter = html.New(html.TabWidth(2), html.WithClasses(true)) 24 + 25 + var dedupeSyntaxStyles = templ.NewOnceHandle() 26 + 27 + func renderPostAsJSON(post *models.Post) string { 28 + contents, err := json.MarshalIndent(post.Properties, "", " ") 29 + if err != nil { 30 + panic(err) 31 + } 32 + 33 + iterator, err := lexer.Tokenise(nil, string(contents)) 34 + 35 + var buf bytes.Buffer 36 + formatter.Format(&buf, style, iterator) 37 + 38 + return buf.String() 39 + } 40 + 41 + func generateSyntaxClassNames() string { 42 + var buf bytes.Buffer 43 + if err := formatter.WriteCSS(&buf, style); err != nil { 44 + panic(err) 45 + } 46 + 47 + return "<style>" + buf.String() + "</style>" 48 + } 49 + 50 + func syntaxStyleTag() templ.Component { 51 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 52 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 53 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 54 + if !templ_7745c5c3_IsBuffer { 55 + defer func() { 56 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 57 + if templ_7745c5c3_Err == nil { 58 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 59 + } 60 + }() 61 + } 62 + ctx = templ.InitializeContext(ctx) 63 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 64 + if templ_7745c5c3_Var1 == nil { 65 + templ_7745c5c3_Var1 = templ.NopComponent 66 + } 67 + ctx = templ.ClearChildren(ctx) 68 + templ_7745c5c3_Err = templ.Raw(generateSyntaxClassNames()).Render(ctx, templ_7745c5c3_Buffer) 69 + if templ_7745c5c3_Err != nil { 70 + return templ_7745c5c3_Err 71 + } 72 + return templ_7745c5c3_Err 73 + }) 74 + } 75 + 76 + func Unsupported(post *models.Post) templ.Component { 77 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 78 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 79 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 80 + if !templ_7745c5c3_IsBuffer { 81 + defer func() { 82 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 83 + if templ_7745c5c3_Err == nil { 84 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 85 + } 86 + }() 87 + } 88 + ctx = templ.InitializeContext(ctx) 89 + templ_7745c5c3_Var2 := templ.GetChildren(ctx) 90 + if templ_7745c5c3_Var2 == nil { 91 + templ_7745c5c3_Var2 = templ.NopComponent 92 + } 93 + ctx = templ.ClearChildren(ctx) 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\">") 95 + if templ_7745c5c3_Err != nil { 96 + return templ_7745c5c3_Err 97 + } 98 + templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 99 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 100 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 101 + if !templ_7745c5c3_IsBuffer { 102 + defer func() { 103 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 104 + if templ_7745c5c3_Err == nil { 105 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 106 + } 107 + }() 108 + } 109 + ctx = templ.InitializeContext(ctx) 110 + templ_7745c5c3_Err = syntaxStyleTag().Render(ctx, templ_7745c5c3_Buffer) 111 + if templ_7745c5c3_Err != nil { 112 + return templ_7745c5c3_Err 113 + } 114 + return templ_7745c5c3_Err 115 + }) 116 + templ_7745c5c3_Err = dedupeSyntaxStyles.Once().Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer) 117 + if templ_7745c5c3_Err != nil { 118 + return templ_7745c5c3_Err 119 + } 120 + templ_7745c5c3_Err = templ.Raw(renderPostAsJSON(post)).Render(ctx, templ_7745c5c3_Buffer) 121 + if templ_7745c5c3_Err != nil { 122 + return templ_7745c5c3_Err 123 + } 124 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") 125 + if templ_7745c5c3_Err != nil { 126 + return templ_7745c5c3_Err 127 + } 128 + return templ_7745c5c3_Err 129 + }) 130 + }
+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 + }
+22
html/layouts/default.templ
··· 1 + package layouts 2 + 3 + import ( 4 + "github.com/puregarlic/space/html/components" 5 + "net/http" 6 + ) 7 + 8 + func RenderDefault(title string, page templ.Component) http.Handler { 9 + document := Default(title, page) 10 + 11 + return templ.Handler(document) 12 + } 13 + 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> 22 + }
+62
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 ( 12 + "github.com/puregarlic/space/html/components" 13 + "net/http" 14 + ) 15 + 16 + func RenderDefault(title string, page templ.Component) http.Handler { 17 + document := Default(title, page) 18 + 19 + return templ.Handler(document) 20 + } 21 + 22 + func Default(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\">") 49 + if templ_7745c5c3_Err != nil { 50 + return templ_7745c5c3_Err 51 + } 52 + templ_7745c5c3_Err = body.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("</body></html>") 57 + if templ_7745c5c3_Err != nil { 58 + return templ_7745c5c3_Err 59 + } 60 + return templ_7745c5c3_Err 61 + }) 62 + }
+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 + }
+63
html/pages/auth.templ
··· 1 + package pages 2 + 3 + import ( 4 + "go.hacdias.com/indielib/indieauth" 5 + "strings" 6 + ) 7 + 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> 63 + }
+267
html/pages/auth_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package pages 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 + "go.hacdias.com/indielib/indieauth" 13 + "strings" 14 + ) 15 + 16 + func Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata, nonceId string, nonce string) templ.Component { 17 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 18 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 19 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 20 + if !templ_7745c5c3_IsBuffer { 21 + defer func() { 22 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 23 + if templ_7745c5c3_Err == nil { 24 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 25 + } 26 + }() 27 + } 28 + ctx = templ.InitializeContext(ctx) 29 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 30 + if templ_7745c5c3_Var1 == nil { 31 + templ_7745c5c3_Var1 = templ.NopComponent 32 + } 33 + ctx = templ.ClearChildren(ctx) 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\">") 35 + if templ_7745c5c3_Err != nil { 36 + return templ_7745c5c3_Err 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 + } 43 + if len(app.Logo) > 0 { 44 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<img class=\"max-w-12\" src=\"") 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(app.Logo) 50 + if templ_7745c5c3_Err != nil { 51 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 16, Col: 42} 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("\">") 58 + if templ_7745c5c3_Err != nil { 59 + return templ_7745c5c3_Err 60 + } 61 + } 62 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div><h2 class=\"font-bold text-lg\">") 63 + if templ_7745c5c3_Err != nil { 64 + return templ_7745c5c3_Err 65 + } 66 + var templ_7745c5c3_Var3 string 67 + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(app.Name) 68 + if templ_7745c5c3_Err != nil { 69 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 19, Col: 46} 70 + } 71 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 72 + if templ_7745c5c3_Err != nil { 73 + return templ_7745c5c3_Err 74 + } 75 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h2>") 76 + if templ_7745c5c3_Err != nil { 77 + return templ_7745c5c3_Err 78 + } 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 + } 97 + } 98 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>") 99 + if templ_7745c5c3_Err != nil { 100 + return templ_7745c5c3_Err 101 + } 102 + } else { 103 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h2 class=\"px-6 font-bold text-subtle\">unidentified client</h2>") 104 + if templ_7745c5c3_Err != nil { 105 + return templ_7745c5c3_Err 106 + } 107 + } 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\">") 109 + if templ_7745c5c3_Err != nil { 110 + return templ_7745c5c3_Err 111 + } 112 + var templ_7745c5c3_Var5 string 113 + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 114 + if templ_7745c5c3_Err != nil { 115 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 30, Col: 101} 116 + } 117 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 118 + if templ_7745c5c3_Err != nil { 119 + return templ_7745c5c3_Err 120 + } 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\">") 122 + if templ_7745c5c3_Err != nil { 123 + return templ_7745c5c3_Err 124 + } 125 + var templ_7745c5c3_Var6 string 126 + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 127 + if templ_7745c5c3_Err != nil { 128 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 32, Col: 93} 129 + } 130 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 131 + if templ_7745c5c3_Err != nil { 132 + return templ_7745c5c3_Err 133 + } 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\">") 135 + if templ_7745c5c3_Err != nil { 136 + return templ_7745c5c3_Err 137 + } 138 + for _, scope := range req.Scopes { 139 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"px-2 py-1 text-sm bg-pine rounded\">") 140 + if templ_7745c5c3_Err != nil { 141 + return templ_7745c5c3_Err 142 + } 143 + var templ_7745c5c3_Var7 string 144 + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(scope) 145 + if templ_7745c5c3_Err != nil { 146 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 36, Col: 59} 147 + } 148 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 149 + if templ_7745c5c3_Err != nil { 150 + return templ_7745c5c3_Err 151 + } 152 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>") 153 + if templ_7745c5c3_Err != nil { 154 + return templ_7745c5c3_Err 155 + } 156 + } 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=\"") 158 + if templ_7745c5c3_Err != nil { 159 + return templ_7745c5c3_Err 160 + } 161 + var templ_7745c5c3_Var8 string 162 + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(req.Scopes, " ")) 163 + if templ_7745c5c3_Err != nil { 164 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 43, Col: 74} 165 + } 166 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) 167 + if templ_7745c5c3_Err != nil { 168 + return templ_7745c5c3_Err 169 + } 170 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"redirect_uri\" value=\"") 171 + if templ_7745c5c3_Err != nil { 172 + return templ_7745c5c3_Err 173 + } 174 + var templ_7745c5c3_Var9 string 175 + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 176 + if templ_7745c5c3_Err != nil { 177 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 44, Col: 67} 178 + } 179 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) 180 + if templ_7745c5c3_Err != nil { 181 + return templ_7745c5c3_Err 182 + } 183 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"client_id\" value=\"") 184 + if templ_7745c5c3_Err != nil { 185 + return templ_7745c5c3_Err 186 + } 187 + var templ_7745c5c3_Var10 string 188 + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 189 + if templ_7745c5c3_Err != nil { 190 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 45, Col: 61} 191 + } 192 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) 193 + if templ_7745c5c3_Err != nil { 194 + return templ_7745c5c3_Err 195 + } 196 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"state\" value=\"") 197 + if templ_7745c5c3_Err != nil { 198 + return templ_7745c5c3_Err 199 + } 200 + var templ_7745c5c3_Var11 string 201 + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(req.State) 202 + if templ_7745c5c3_Err != nil { 203 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 46, Col: 54} 204 + } 205 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) 206 + if templ_7745c5c3_Err != nil { 207 + return templ_7745c5c3_Err 208 + } 209 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"code_challenge\" value=\"") 210 + if templ_7745c5c3_Err != nil { 211 + return templ_7745c5c3_Err 212 + } 213 + var templ_7745c5c3_Var12 string 214 + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallenge) 215 + if templ_7745c5c3_Err != nil { 216 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 47, Col: 71} 217 + } 218 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) 219 + if templ_7745c5c3_Err != nil { 220 + return templ_7745c5c3_Err 221 + } 222 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"code_challenge_method\" value=\"") 223 + if templ_7745c5c3_Err != nil { 224 + return templ_7745c5c3_Err 225 + } 226 + var templ_7745c5c3_Var13 string 227 + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallengeMethod) 228 + if templ_7745c5c3_Err != nil { 229 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 48, Col: 84} 230 + } 231 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) 232 + if templ_7745c5c3_Err != nil { 233 + return templ_7745c5c3_Err 234 + } 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>") 262 + if templ_7745c5c3_Err != nil { 263 + return templ_7745c5c3_Err 264 + } 265 + return templ_7745c5c3_Err 266 + }) 267 + }
+23
html/pages/home.templ
··· 1 + package pages 2 + 3 + import ( 4 + p "github.com/puregarlic/space/html/components/posts" 5 + "github.com/puregarlic/space/models" 6 + ) 7 + 8 + templ Home(posts []*models.Post) { 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> 23 + }
+69
html/pages/home_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package pages 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 + p "github.com/puregarlic/space/html/components/posts" 13 + "github.com/puregarlic/space/models" 14 + ) 15 + 16 + func Home(posts []*models.Post) templ.Component { 17 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 18 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 19 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 20 + if !templ_7745c5c3_IsBuffer { 21 + defer func() { 22 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 23 + if templ_7745c5c3_Err == nil { 24 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 25 + } 26 + }() 27 + } 28 + ctx = templ.InitializeContext(ctx) 29 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 30 + if templ_7745c5c3_Var1 == nil { 31 + templ_7745c5c3_Var1 = templ.NopComponent 32 + } 33 + ctx = templ.ClearChildren(ctx) 34 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<ul class=\"flex flex-col gap-6\">") 35 + if templ_7745c5c3_Err != nil { 36 + return templ_7745c5c3_Err 37 + } 38 + if len(posts) > 0 { 39 + for _, post := range posts { 40 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"flex flex-col\">") 41 + if templ_7745c5c3_Err != nil { 42 + return templ_7745c5c3_Err 43 + } 44 + templ_7745c5c3_Err = p.PostFeedHeader(post).Render(ctx, templ_7745c5c3_Buffer) 45 + if templ_7745c5c3_Err != nil { 46 + return templ_7745c5c3_Err 47 + } 48 + templ_7745c5c3_Err = p.PostContent(post).Render(ctx, templ_7745c5c3_Buffer) 49 + if templ_7745c5c3_Err != nil { 50 + return templ_7745c5c3_Err 51 + } 52 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>") 53 + if templ_7745c5c3_Err != nil { 54 + return templ_7745c5c3_Err 55 + } 56 + } 57 + } else { 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 + if templ_7745c5c3_Err != nil { 60 + return templ_7745c5c3_Err 61 + } 62 + } 63 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul>") 64 + if templ_7745c5c3_Err != nil { 65 + return templ_7745c5c3_Err 66 + } 67 + return templ_7745c5c3_Err 68 + }) 69 + }
+27
html/pages/post.templ
··· 1 + package pages 2 + 3 + import ( 4 + "github.com/puregarlic/space/html/components/posts" 5 + "github.com/puregarlic/space/models" 6 + ) 7 + 8 + templ Post(post *models.Post) { 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> 27 + }
+56
html/pages/post_templ.go
··· 1 + // Code generated by templ - DO NOT EDIT. 2 + 3 + // templ: version: v0.2.747 4 + package pages 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/posts" 13 + "github.com/puregarlic/space/models" 14 + ) 15 + 16 + func Post(post *models.Post) templ.Component { 17 + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 18 + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 19 + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 20 + if !templ_7745c5c3_IsBuffer { 21 + defer func() { 22 + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 23 + if templ_7745c5c3_Err == nil { 24 + templ_7745c5c3_Err = templ_7745c5c3_BufErr 25 + } 26 + }() 27 + } 28 + ctx = templ.InitializeContext(ctx) 29 + templ_7745c5c3_Var1 := templ.GetChildren(ctx) 30 + if templ_7745c5c3_Var1 == nil { 31 + templ_7745c5c3_Var1 = templ.NopComponent 32 + } 33 + ctx = templ.ClearChildren(ctx) 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>") 35 + if templ_7745c5c3_Err != nil { 36 + return templ_7745c5c3_Err 37 + } 38 + templ_7745c5c3_Err = posts.PostContent(post).Render(ctx, templ_7745c5c3_Buffer) 39 + if templ_7745c5c3_Err != nil { 40 + return templ_7745c5c3_Err 41 + } 42 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>") 43 + if templ_7745c5c3_Err != nil { 44 + return templ_7745c5c3_Err 45 + } 46 + templ_7745c5c3_Err = posts.PostDetails(post).Render(ctx, templ_7745c5c3_Buffer) 47 + if templ_7745c5c3_Err != nil { 48 + return templ_7745c5c3_Err 49 + } 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>") 51 + if templ_7745c5c3_Err != nil { 52 + return templ_7745c5c3_Err 53 + } 54 + return templ_7745c5c3_Err 55 + }) 56 + }
-241
indieauth.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "crypto/sha256" 6 - "crypto/subtle" 7 - "errors" 8 - "net/http" 9 - "net/url" 10 - "os" 11 - "strings" 12 - "time" 13 - 14 - "github.com/puregarlic/space/pages" 15 - 16 - "github.com/a-h/templ" 17 - "github.com/aidarkhanov/nanoid" 18 - "github.com/golang-jwt/jwt/v5" 19 - "go.hacdias.com/indielib/indieauth" 20 - ) 21 - 22 - // storeAuthorization stores the authorization request and returns a code for it. 23 - // Something such as JWT tokens could be used in a production environment. 24 - func (s *server) storeAuthorization(req *indieauth.AuthenticationRequest) string { 25 - code := nanoid.New() 26 - 27 - s.db.Authorization.Set(code, req, 0) 28 - 29 - return code 30 - } 31 - 32 - type CustomTokenClaims struct { 33 - Scopes []string `json:"scopes"` 34 - jwt.RegisteredClaims 35 - } 36 - 37 - type contextKey string 38 - 39 - const ( 40 - scopesContextKey contextKey = "scopes" 41 - ) 42 - 43 - // authorizationGetHandler handles the GET method for the authorization endpoint. 44 - func (s *server) authorizationGetHandler(w http.ResponseWriter, r *http.Request) { 45 - // In a production server, this page would usually be protected. In order for 46 - // the user to authorize this request, they must be authenticated. This could 47 - // be done in different ways: username/password, passkeys, etc. 48 - 49 - // Parse the authorization request. 50 - req, err := s.ias.ParseAuthorization(r) 51 - if err != nil { 52 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 53 - return 54 - } 55 - 56 - // Do a best effort attempt at fetching more information about the application 57 - // that we can show to the user. Not all applications provide this sort of 58 - // information. 59 - app, _ := s.ias.DiscoverApplicationMetadata(r.Context(), req.ClientID) 60 - 61 - // Here, we just display a small HTML document where the user has to press 62 - // to authorize this request. Please note that this template contains a form 63 - // where we dump all the request information. This makes it possible to reuse 64 - // [indieauth.Server.ParseAuthorization] when the user authorizes the request. 65 - templ.Handler(pages.Auth(req, app)).ServeHTTP(w, r) 66 - } 67 - 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) 71 - } 72 - 73 - // tokenHandler handles the token endpoint. In our case, we only accept the default 74 - // type which is exchanging an authorization code for a token. 75 - func (s *server) tokenHandler(w http.ResponseWriter, r *http.Request) { 76 - if r.Method != http.MethodPost { 77 - httpError(w, http.StatusMethodNotAllowed) 78 - return 79 - } 80 - 81 - if r.Form.Get("grant_type") == "refresh_token" { 82 - // NOTE: this server does not implement refresh tokens. 83 - // https://indieauth.spec.indieweb.org/#refresh-tokens 84 - w.WriteHeader(http.StatusNotImplemented) 85 - return 86 - } 87 - 88 - s.authorizationCodeExchange(w, r, true) 89 - } 90 - 91 - type tokenResponse struct { 92 - Me string `json:"me"` 93 - AccessToken string `json:"access_token,omitempty"` 94 - TokenType string `json:"token_type,omitempty"` 95 - Scope string `json:"scope,omitempty"` 96 - ExpiresIn int64 `json:"expires_in,omitempty"` 97 - } 98 - 99 - // authorizationCodeExchange handles the authorization code exchange. It is used by 100 - // both the authorization handler to exchange the code for the user's profile URL, 101 - // 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) { 103 - if err := r.ParseForm(); err != nil { 104 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 105 - return 106 - } 107 - 108 - // t := s.getAuthorization(r.Form.Get("code")) 109 - req, present := s.db.Authorization.GetAndDelete(r.Form.Get("code")) 110 - if !present { 111 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", "invalid authorization") 112 - return 113 - } 114 - authRequest := req.Value() 115 - 116 - err := s.ias.ValidateTokenExchange(authRequest, r) 117 - if err != nil { 118 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 119 - return 120 - } 121 - 122 - response := &tokenResponse{ 123 - Me: s.profileURL, 124 - } 125 - 126 - scopes := authRequest.Scopes 127 - 128 - if withToken { 129 - now := time.Now() 130 - expiresAt := now.Add(15 * time.Minute) 131 - claims := CustomTokenClaims{ 132 - scopes, 133 - jwt.RegisteredClaims{ 134 - ExpiresAt: jwt.NewNumericDate(expiresAt), 135 - IssuedAt: jwt.NewNumericDate(now), 136 - NotBefore: jwt.NewNumericDate(now), 137 - }, 138 - } 139 - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 140 - 141 - secret := os.Getenv("JWT_SECRET") 142 - jwt, err := token.SignedString([]byte(secret)) 143 - if err != nil { 144 - panic(err) 145 - } 146 - 147 - response.AccessToken = jwt 148 - response.TokenType = "Bearer" 149 - response.ExpiresIn = int64(time.Until(expiresAt).Seconds()) 150 - response.Scope = strings.Join(scopes, " ") 151 - } 152 - 153 - // An actual server may want to include the "profile" in the response if the 154 - // scope "profile" is included. 155 - serveJSON(w, http.StatusOK, response) 156 - } 157 - 158 - func (s *server) authorizationAcceptHandler(w http.ResponseWriter, r *http.Request) { 159 - // Parse authorization information. This only works because our authorization page 160 - // includes all the required information. This can be done in other ways: database, 161 - // whether temporary or not, cookies, etc. 162 - req, err := s.ias.ParseAuthorization(r) 163 - if err != nil { 164 - serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 165 - return 166 - } 167 - 168 - // Generate a random code and persist the information associated to that code. 169 - // You could do this in other ways: database, or JWT tokens, or both, for example. 170 - code := s.storeAuthorization(req) 171 - 172 - // Redirect to client callback. 173 - query := url.Values{} 174 - query.Set("code", code) 175 - query.Set("state", req.State) 176 - http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound) 177 - } 178 - 179 - // mustAuth is a middleware to ensure that the request is authorized. The way this 180 - // works depends on the implementation. It then stores the scopes in the context. 181 - func (s *server) mustAuth(next http.Handler) http.Handler { 182 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 183 - tokenStr := r.Header.Get("Authorization") 184 - tokenStr = strings.TrimPrefix(tokenStr, "Bearer") 185 - tokenStr = strings.TrimSpace(tokenStr) 186 - 187 - if len(tokenStr) <= 0 { 188 - serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "no credentials") 189 - return 190 - } 191 - 192 - token, err := jwt.ParseWithClaims(tokenStr, &CustomTokenClaims{}, func(t *jwt.Token) (interface{}, error) { 193 - return []byte(os.Getenv("JWT_SECRET")), nil 194 - }) 195 - 196 - if err != nil { 197 - serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "invalid token") 198 - return 199 - } else if claims, ok := token.Claims.(*CustomTokenClaims); ok { 200 - ctx := context.WithValue(r.Context(), scopesContextKey, claims.Scopes) 201 - next.ServeHTTP(w, r.WithContext(ctx)) 202 - return 203 - } else { 204 - serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "malformed claims") 205 - return 206 - } 207 - }) 208 - } 209 - 210 - func (s *server) mustBasicAuth(next http.Handler) http.Handler { 211 - user, ok := os.LookupEnv("ADMIN_USERNAME") 212 - if !ok { 213 - panic(errors.New("ADMIN_USERNAME is not set, cannot start")) 214 - } 215 - 216 - pass, ok := os.LookupEnv("ADMIN_PASSWORD") 217 - if !ok { 218 - panic(errors.New("ADMIN_PASSWORD is not set, cannot start")) 219 - } 220 - 221 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 222 - username, password, ok := r.BasicAuth() 223 - if ok { 224 - usernameHash := sha256.Sum256([]byte(username)) 225 - passwordHash := sha256.Sum256([]byte(password)) 226 - expectedUsernameHash := sha256.Sum256([]byte(user)) 227 - expectedPasswordHash := sha256.Sum256([]byte(pass)) 228 - 229 - usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) 230 - passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) 231 - 232 - if usernameMatch && passwordMatch { 233 - next.ServeHTTP(w, r) 234 - return 235 - } 236 - } 237 - 238 - w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) 239 - http.Error(w, "Unauthorized", http.StatusUnauthorized) 240 - }) 241 - }
+42 -160
main.go
··· 1 1 package main 2 2 3 + //go:generate templ generate 4 + //go:generate deno run --allow-all npm:tailwindcss -i config/main.css -o static/styles.css -c config/tailwind.config.ts --minify 5 + 3 6 import ( 4 - "encoding/json" 5 7 "os" 6 8 "time" 7 9 ··· 9 11 "net/http" 10 12 "strconv" 11 13 12 - "github.com/puregarlic/space/db" 14 + "github.com/puregarlic/space/handlers" 13 15 "github.com/puregarlic/space/models" 14 - "github.com/puregarlic/space/pages" 16 + "github.com/puregarlic/space/storage" 15 17 16 - "github.com/a-h/templ" 17 18 "github.com/go-chi/chi/v5" 18 19 "github.com/go-chi/chi/v5/middleware" 20 + "github.com/go-chi/cors" 19 21 "github.com/go-chi/httprate" 20 22 21 23 "go.hacdias.com/indielib/indieauth" 22 - "go.hacdias.com/indielib/microformats" 23 - "go.hacdias.com/indielib/micropub" 24 24 25 25 _ "github.com/joho/godotenv/autoload" 26 26 ) 27 27 28 28 func main() { 29 - var port int 30 - if portStr, ok := os.LookupEnv("PORT"); !ok { 31 - port = 80 32 - } else { 33 - portInt, err := strconv.Atoi(portStr) 34 - if err != nil { 35 - log.Fatal(err) 36 - } 29 + port, profileURL := validateRuntimeConfiguration() 30 + defer storage.CleanupCaches() 37 31 38 - port = portInt 39 - } 40 - 41 - profileURL, ok := os.LookupEnv("PROFILE_URL") 42 - if !ok { 43 - profileURL = "http://localhost/" 44 - } 45 - 46 - // Validate the given Client ID before starting the HTTP server. 47 - err := indieauth.IsValidProfileURL(profileURL) 48 - if err != nil { 49 - log.Fatal(err) 50 - } 51 - 52 - // Setup storage handlers 53 - store := db.NewStorage() 54 - defer store.Cleanup() 55 - 56 - // Create a new client. 57 - s := &server{ 58 - profileURL: profileURL, 59 - ias: indieauth.NewServer(true, nil), 60 - db: store, 61 - } 32 + storage.GORM().AutoMigrate(&models.Post{}) 62 33 63 34 r := chi.NewRouter() 64 35 ··· 66 37 r.Use(middleware.RealIP) 67 38 r.Use(middleware.Logger) 68 39 r.Use(middleware.Recoverer) 69 - 70 40 r.Use(httprate.LimitByIP(100, 1*time.Minute)) 71 41 72 - // Static resources 73 - r.Get("/static/*", http.StripPrefix("/static", http.FileServer(http.Dir("static"))).ServeHTTP) 42 + // CORS be enabled for browser-based agents to fetch `rel` elements. 43 + // We'll enable it just on the root route since it should be used as the profile URL 44 + r.With(cors.AllowAll().Handler).Get("/", handlers.ServeHomePage) 74 45 75 - // Pages 76 - r.Get("/", s.serveHomeTemplate) 77 - r.Get("/posts/{slug}", s.servePostTemplate) 46 + // Content pages 47 + r.Get("/posts/{slug}", handlers.ServePostPage) 78 48 79 - // IndieAuth handlers 80 - r.Group(func(r chi.Router) { 81 - r.Post("/token", s.tokenHandler) 82 - r.Post("/authorization", s.authorizationPostHandler) 83 - r.Post("/authorization/accept", s.authorizationAcceptHandler) 49 + // Static asset handlers 50 + r.Get("/media/*", handlers.ServeMedia) 51 + r.Get("/static/*", http.StripPrefix( 52 + "/static", 53 + http.FileServer(http.Dir("static")), 54 + ).ServeHTTP) 84 55 85 - // User authentication portal 86 - r.With(s.mustBasicAuth).Get("/authorization", s.authorizationGetHandler) 87 - }) 88 - 89 - // Micropub handler 90 - r.Route("/micropub", func(r chi.Router) { 91 - r.Use(s.mustAuth) 92 - r.Get("/", s.serveMicropub) 93 - r.Post("/", s.serveMicropub) 94 - }) 56 + // Service handlers 57 + handlers.AttachIndieAuth(r, "/authorization", profileURL) 58 + handlers.AttachMicropub(r, "/micropub", profileURL) 95 59 96 60 // Start it! 97 61 log.Printf("Listening on http://localhost:%d", port) ··· 101 65 } 102 66 } 103 67 104 - type server struct { 105 - profileURL string 106 - ias *indieauth.Server 107 - db *db.Storage 108 - } 109 - 110 - func (s *server) serveHomeTemplate(w http.ResponseWriter, r *http.Request) { 111 - // q := query.NewQuery( 112 - // string(db.PostCollection), 113 - // ).Sort(query.SortOption{ 114 - // Field: "createdAt", 115 - // Direction: -1, 116 - // }).Limit(10) 117 - 118 - // docs, err := s.db.Docs.FindAll(q) 119 - // if err != nil { 120 - // httpError(w, http.StatusInternalServerError) 121 - // panic(err) 122 - // } 68 + func validateRuntimeConfiguration() (portNumber int, profileURL string) { 69 + var port int 70 + if portStr, ok := os.LookupEnv("PORT"); !ok { 71 + port = 80 72 + } else { 73 + portInt, err := strconv.Atoi(portStr) 74 + if err != nil { 75 + log.Fatal(err) 76 + } 123 77 124 - posts := make([]*models.Post, 0) 125 - result := s.db.Db.Limit(10).Find(&posts) 126 - if result.Error != nil { 127 - panic(result.Error) 78 + port = portInt 128 79 } 129 80 130 - // posts := make([]*models.Post, len(docs)) 131 - // for i, doc := range docs { 132 - // id := doc.ObjectId() 133 - // post := &models.Post{ 134 - // ID: id, 135 - // } 81 + profileURL, ok := os.LookupEnv("PROFILE_URL") 82 + if !ok { 83 + profileURL = "http://localhost/" 84 + } 136 85 137 - // if err := doc.Unmarshal(post); err != nil { 138 - // httpError(w, http.StatusInternalServerError) 139 - // panic(err) 140 - // } 141 - 142 - // post.ID = id 143 - 144 - // posts[i] = post 145 - // } 146 - 147 - templ.Handler(pages.Home(s.profileURL, posts)).ServeHTTP(w, r) 148 - } 149 - 150 - func (s *server) servePostTemplate(w http.ResponseWriter, r *http.Request) { 151 - id := chi.URLParam(r, "slug") 152 - post := &models.Post{} 153 - 154 - // doc, err := s.db.Docs.FindById(string(db.PostCollection), id) 155 - // if err != nil { 156 - // httpError(w, http.StatusInternalServerError) 157 - // return 158 - // } else if doc == nil { 159 - // httpError(w, http.StatusNotFound) 160 - // return 161 - // } 162 - 163 - // postUlid, err := ulid.ParseString(id) 164 - // if err != nil { 165 - // panic(err) 166 - // } 167 - result := s.db.Db.First(post, "id = ?", id) 168 - 169 - if result.RowsAffected == 0 { 170 - httpError(w, http.StatusNotFound) 171 - return 86 + // Validate the given Client ID before starting the HTTP server. 87 + err := indieauth.IsValidProfileURL(profileURL) 88 + if err != nil { 89 + log.Fatal(err) 172 90 } 173 91 174 - // if err := doc.Unmarshal(post); err != nil { 175 - // httpError(w, http.StatusInternalServerError) 176 - // return 177 - // } 178 - 179 - templ.Handler(pages.Post(post)).ServeHTTP(w, r) 180 - } 181 - 182 - func (s *server) serveMicropub(w http.ResponseWriter, r *http.Request) { 183 - micropub.NewHandler( 184 - &micropubImplementation{s}, 185 - micropub.WithGetPostTypes(func() []micropub.PostType { 186 - return []micropub.PostType{ 187 - { 188 - Name: "Post", 189 - Type: string(microformats.TypeNote), 190 - }, 191 - } 192 - }), 193 - ).ServeHTTP(w, r) 194 - } 195 - 196 - func httpError(w http.ResponseWriter, status int) { 197 - http.Error(w, http.StatusText(status), status) 198 - } 199 - 200 - func serveJSON(w http.ResponseWriter, code int, data interface{}) { 201 - w.Header().Set("Content-Type", "application/json; charset=utf-8") 202 - w.WriteHeader(code) 203 - _ = json.NewEncoder(w).Encode(data) 204 - } 205 - 206 - func serveErrorJSON(w http.ResponseWriter, code int, err, errDescription string) { 207 - serveJSON(w, code, map[string]string{ 208 - "error": err, 209 - "error_description": errDescription, 210 - }) 92 + return port, profileURL 211 93 }
-229
micropub.go
··· 1 - package main 2 - 3 - import ( 4 - "encoding/json" 5 - "errors" 6 - "fmt" 7 - "net/http" 8 - urlpkg "net/url" 9 - "reflect" 10 - "strings" 11 - 12 - "github.com/puregarlic/space/models" 13 - "github.com/samber/lo" 14 - 15 - "go.hacdias.com/indielib/micropub" 16 - ) 17 - 18 - type micropubImplementation struct { 19 - *server 20 - } 21 - 22 - func postIdFromUrlPath(path string) string { 23 - return strings.TrimPrefix(path, "/posts/") 24 - } 25 - 26 - func (s *micropubImplementation) HasScope(r *http.Request, scope string) bool { 27 - v := r.Context().Value(scopesContextKey) 28 - if scopes, ok := v.([]string); ok { 29 - for _, sc := range scopes { 30 - if sc == scope { 31 - return true 32 - } 33 - } 34 - } 35 - 36 - return false 37 - } 38 - 39 - func (s *micropubImplementation) Source(urlStr string) (map[string]any, error) { 40 - url, err := urlpkg.Parse(urlStr) 41 - if err != nil { 42 - return nil, fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 43 - } 44 - 45 - id := postIdFromUrlPath(url.Path) 46 - post := &models.Post{} 47 - 48 - res := s.server.db.Db.Find(post, "id = ?", id) 49 - if res.Error != nil { 50 - panic(res.Error) 51 - } else if res.RowsAffected == 0 { 52 - return nil, micropub.ErrNotFound 53 - } 54 - 55 - return map[string]any{ 56 - "type": []string{post.Type}, 57 - "properties": post.Properties, 58 - }, nil 59 - } 60 - 61 - func (s *micropubImplementation) SourceMany(limit, offset int) ([]map[string]any, error) { 62 - return nil, micropub.ErrNotImplemented 63 - } 64 - 65 - func (s *micropubImplementation) Create(req *micropub.Request) (string, error) { 66 - props, err := json.Marshal(req.Properties) 67 - if err != nil { 68 - return "", err 69 - } 70 - 71 - post := &models.Post{ 72 - ID: models.NewULID(), 73 - Type: req.Type, 74 - Properties: props, 75 - } 76 - 77 - res := s.server.db.Db.Create(post) 78 - if res.Error != nil { 79 - return "", res.Error 80 - } 81 - 82 - return s.profileURL + "posts/" + post.ID.String(), nil 83 - } 84 - 85 - func (s *micropubImplementation) Update(req *micropub.Request) (string, error) { 86 - url, err := urlpkg.Parse(req.URL) 87 - if err != nil { 88 - return "", fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 89 - } 90 - 91 - id := postIdFromUrlPath(url.Path) 92 - post := &models.Post{} 93 - 94 - res := s.server.db.Db.Find(post, "id = ?", id) 95 - if res.Error != nil { 96 - panic(res.Error) 97 - } else if res.RowsAffected != 1 { 98 - return "", micropub.ErrNotFound 99 - } 100 - 101 - newProps, err := updateProperties(json.RawMessage(post.Properties), req) 102 - if err != nil { 103 - panic(err) 104 - } 105 - 106 - post.Properties = newProps 107 - 108 - s.server.db.Db.Save(post) 109 - 110 - return s.profileURL + url.Path, nil 111 - } 112 - 113 - func (s *micropubImplementation) Delete(urlStr string) error { 114 - url, err := urlpkg.Parse(urlStr) 115 - if err != nil { 116 - return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 117 - } 118 - 119 - id := postIdFromUrlPath(url.Path) 120 - 121 - res := s.server.db.Db.Delete(&models.Post{}, "id = ?", id) 122 - if res.Error != nil { 123 - panic(res.Error) 124 - } else if res.RowsAffected == 0 { 125 - return fmt.Errorf("%w: %w", micropub.ErrNotFound, err) 126 - } 127 - 128 - return nil 129 - } 130 - 131 - func (s *micropubImplementation) Undelete(urlStr string) error { 132 - url, err := urlpkg.Parse(urlStr) 133 - if err != nil { 134 - return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 135 - } 136 - 137 - id := postIdFromUrlPath(url.Path) 138 - res := s.server.db.Db.Unscoped().Model(&models.Post{}).Where("id = ?", id).Update("deleted_at", nil) 139 - if res.Error != nil { 140 - return res.Error 141 - } else if res.RowsAffected != 1 { 142 - return micropub.ErrNotFound 143 - } 144 - 145 - return nil 146 - } 147 - 148 - // updateProperties applies the updates (additions, deletions, replacements) 149 - // in the given [micropub.Request] to a set of existing microformats properties. 150 - func updateProperties(props json.RawMessage, req *micropub.Request) ([]byte, error) { 151 - properties := make(map[string][]any) 152 - if err := json.Unmarshal(props, &properties); err != nil { 153 - panic(err) 154 - } 155 - 156 - if req.Updates.Replace != nil { 157 - for key, value := range req.Updates.Replace { 158 - properties[key] = value 159 - } 160 - } 161 - 162 - if req.Updates.Add != nil { 163 - for key, value := range req.Updates.Add { 164 - switch key { 165 - case "name": 166 - return nil, errors.New("cannot add a new name") 167 - case "content": 168 - return nil, errors.New("cannot add content") 169 - default: 170 - if key == "published" { 171 - if _, ok := properties["published"]; ok { 172 - return nil, errors.New("cannot replace published through add method") 173 - } 174 - } 175 - 176 - if _, ok := properties[key]; !ok { 177 - properties[key] = []any{} 178 - } 179 - 180 - properties[key] = append(properties[key], value...) 181 - } 182 - } 183 - } 184 - 185 - if req.Updates.Delete != nil { 186 - if reflect.TypeOf(req.Updates.Delete).Kind() == reflect.Slice { 187 - toDelete, ok := req.Updates.Delete.([]any) 188 - if !ok { 189 - return nil, errors.New("invalid delete array") 190 - } 191 - 192 - for _, key := range toDelete { 193 - delete(properties, fmt.Sprint(key)) 194 - } 195 - } else { 196 - toDelete, ok := req.Updates.Delete.(map[string]any) 197 - if !ok { 198 - return nil, fmt.Errorf("invalid delete object: expected map[string]any, got: %s", reflect.TypeOf(req.Updates.Delete)) 199 - } 200 - 201 - for key, v := range toDelete { 202 - value, ok := v.([]any) 203 - if !ok { 204 - return nil, fmt.Errorf("invalid value: expected []any, got: %s", reflect.TypeOf(value)) 205 - } 206 - 207 - if _, ok := properties[key]; !ok { 208 - properties[key] = []any{} 209 - } 210 - 211 - properties[key] = lo.Filter(properties[key], func(ss any, _ int) bool { 212 - for _, s := range value { 213 - if s == ss { 214 - return false 215 - } 216 - } 217 - return true 218 - }) 219 - } 220 - } 221 - } 222 - 223 - propJson, err := json.Marshal(&properties) 224 - if err != nil { 225 - panic(err) 226 - } 227 - 228 - return propJson, nil 229 - }
+8 -2
models/post.go
··· 3 3 import ( 4 4 "time" 5 5 6 + "go.hacdias.com/indielib/microformats" 6 7 "gorm.io/datatypes" 7 8 "gorm.io/gorm" 8 9 ) ··· 10 11 type Post struct { 11 12 ID ULID `gorm:"primaryKey;unique"` 12 13 13 - Type string 14 - Properties datatypes.JSON 14 + Type string 15 + MicroformatType microformats.Type 16 + Properties datatypes.JSON 15 17 16 18 CreatedAt time.Time 17 19 UpdatedAt time.Time 18 20 DeletedAt gorm.DeletedAt `gorm:"index"` 19 21 } 22 + 23 + func (p *Post) Timestamp() string { 24 + return p.CreatedAt.Format("01/02/2006 at 3:04 PM") 25 + }
-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 - }
-56
pages/auth.templ
··· 1 - package pages 2 - 3 - import "strings" 4 - 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> 56 - }
-226
pages/auth_templ.go
··· 1 - // Code generated by templ - DO NOT EDIT. 2 - 3 - // templ: version: v0.2.747 4 - package pages 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 "strings" 12 - 13 - import "go.hacdias.com/indielib/indieauth" 14 - 15 - func Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata) templ.Component { 16 - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 17 - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 18 - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 19 - if !templ_7745c5c3_IsBuffer { 20 - defer func() { 21 - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 22 - if templ_7745c5c3_Err == nil { 23 - templ_7745c5c3_Err = templ_7745c5c3_BufErr 24 - } 25 - }() 26 - } 27 - ctx = templ.InitializeContext(ctx) 28 - templ_7745c5c3_Var1 := templ.GetChildren(ctx) 29 - if templ_7745c5c3_Var1 == nil { 30 - templ_7745c5c3_Var1 = templ.NopComponent 31 - } 32 - 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 - if templ_7745c5c3_Err != nil { 35 - return templ_7745c5c3_Err 36 - } 37 - if app != nil { 38 - if len(app.Logo) > 0 { 39 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<img style=\"width: 1em; vertical-align: middle\" src=\"") 40 - if templ_7745c5c3_Err != nil { 41 - return templ_7745c5c3_Err 42 - } 43 - var templ_7745c5c3_Var2 string 44 - templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(app.Logo) 45 - if templ_7745c5c3_Err != nil { 46 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 21, Col: 72} 47 - } 48 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 49 - if templ_7745c5c3_Err != nil { 50 - return templ_7745c5c3_Err 51 - } 52 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") 53 - if templ_7745c5c3_Err != nil { 54 - return templ_7745c5c3_Err 55 - } 56 - } 57 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <strong>") 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(app.Name) 63 - if templ_7745c5c3_Err != nil { 64 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 24, Col: 26} 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("</strong> by ") 71 - if templ_7745c5c3_Err != nil { 72 - return templ_7745c5c3_Err 73 - } 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: `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 82 - } 83 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(":") 84 - if templ_7745c5c3_Err != nil { 85 - return templ_7745c5c3_Err 86 - } 87 - } else { 88 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("the following client:") 89 - if templ_7745c5c3_Err != nil { 90 - return templ_7745c5c3_Err 91 - } 92 - } 93 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><ul><li><strong>Redirect:</strong> <code>") 94 - if templ_7745c5c3_Err != nil { 95 - return templ_7745c5c3_Err 96 - } 97 - var templ_7745c5c3_Var5 string 98 - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 99 - if templ_7745c5c3_Err != nil { 100 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 31, Col: 57} 101 - } 102 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 103 - if templ_7745c5c3_Err != nil { 104 - return templ_7745c5c3_Err 105 - } 106 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</code></li><li><strong>Client:</strong> <code>") 107 - if templ_7745c5c3_Err != nil { 108 - return templ_7745c5c3_Err 109 - } 110 - var templ_7745c5c3_Var6 string 111 - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 112 - if templ_7745c5c3_Err != nil { 113 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 32, Col: 58} 114 - } 115 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 116 - if templ_7745c5c3_Err != nil { 117 - return templ_7745c5c3_Err 118 - } 119 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</code></li></ul><p>For the following scopes: ") 120 - if templ_7745c5c3_Err != nil { 121 - return templ_7745c5c3_Err 122 - } 123 - for _, scope := range req.Scopes { 124 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<code>") 125 - if templ_7745c5c3_Err != nil { 126 - return templ_7745c5c3_Err 127 - } 128 - var templ_7745c5c3_Var7 string 129 - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(scope) 130 - if templ_7745c5c3_Err != nil { 131 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 37, Col: 21} 132 - } 133 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 134 - if templ_7745c5c3_Err != nil { 135 - return templ_7745c5c3_Err 136 - } 137 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</code> ") 138 - if templ_7745c5c3_Err != nil { 139 - return templ_7745c5c3_Err 140 - } 141 - } 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=\"") 143 - if templ_7745c5c3_Err != nil { 144 - return templ_7745c5c3_Err 145 - } 146 - var templ_7745c5c3_Var8 string 147 - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(req.Scopes, " ")) 148 - if templ_7745c5c3_Err != nil { 149 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 43, Col: 77} 150 - } 151 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) 152 - if templ_7745c5c3_Err != nil { 153 - return templ_7745c5c3_Err 154 - } 155 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"redirect_uri\" value=\"") 156 - if templ_7745c5c3_Err != nil { 157 - return templ_7745c5c3_Err 158 - } 159 - var templ_7745c5c3_Var9 string 160 - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 161 - if templ_7745c5c3_Err != nil { 162 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 44, Col: 70} 163 - } 164 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) 165 - if templ_7745c5c3_Err != nil { 166 - return templ_7745c5c3_Err 167 - } 168 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"client_id\" value=\"") 169 - if templ_7745c5c3_Err != nil { 170 - return templ_7745c5c3_Err 171 - } 172 - var templ_7745c5c3_Var10 string 173 - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 174 - if templ_7745c5c3_Err != nil { 175 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 45, Col: 64} 176 - } 177 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) 178 - if templ_7745c5c3_Err != nil { 179 - return templ_7745c5c3_Err 180 - } 181 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"state\" value=\"") 182 - if templ_7745c5c3_Err != nil { 183 - return templ_7745c5c3_Err 184 - } 185 - var templ_7745c5c3_Var11 string 186 - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(req.State) 187 - if templ_7745c5c3_Err != nil { 188 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 46, Col: 57} 189 - } 190 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) 191 - if templ_7745c5c3_Err != nil { 192 - return templ_7745c5c3_Err 193 - } 194 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"code_challenge\" value=\"") 195 - if templ_7745c5c3_Err != nil { 196 - return templ_7745c5c3_Err 197 - } 198 - var templ_7745c5c3_Var12 string 199 - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallenge) 200 - if templ_7745c5c3_Err != nil { 201 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 47, Col: 74} 202 - } 203 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) 204 - if templ_7745c5c3_Err != nil { 205 - return templ_7745c5c3_Err 206 - } 207 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"code_challenge_method\" value=\"") 208 - if templ_7745c5c3_Err != nil { 209 - return templ_7745c5c3_Err 210 - } 211 - var templ_7745c5c3_Var13 string 212 - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallengeMethod) 213 - if templ_7745c5c3_Err != nil { 214 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/auth.templ`, Line: 48, Col: 87} 215 - } 216 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) 217 - if templ_7745c5c3_Err != nil { 218 - return templ_7745c5c3_Err 219 - } 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>") 221 - if templ_7745c5c3_Err != nil { 222 - return templ_7745c5c3_Err 223 - } 224 - return templ_7745c5c3_Err 225 - }) 226 - }
-42
pages/home.templ
··· 1 - package pages 2 - 3 - import "github.com/puregarlic/space/models" 4 - import "fmt" 5 - import "encoding/json" 6 - 7 - func getPostPropertyValue(post *models.Post, name string) any { 8 - props := make(map[string]any) 9 - if err := json.Unmarshal(post.Properties, &props); err != nil { 10 - panic(err) 11 - } 12 - 13 - return props[name] 14 - } 15 - 16 - templ Home(profileUrl string, posts []*models.Post) { 17 - <!DOCTYPE html> 18 - <html> 19 - <head> 20 - <title>Micropub and IndieAuth Server Demo</title> 21 - <link rel="authorization_endpoint" href="/authorization"> 22 - <link rel="token_endpoint" href="/token"> 23 - <link rel="micropub" href="/micropub"> 24 - 25 - // <link rel="stylesheet" href="/static/styles.css" /> 26 - </head> 27 - <body> 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 - 32 - <h2>Posts</h2> 33 - 34 - <p>You can create posts using a Micropub client.</p> 35 - <ul> 36 - for _, post := range posts { 37 - <li><a href={ templ.URL("/posts/" + post.ID.String()) }>{ post.ID.String() }</a> - { fmt.Sprint(getPostPropertyValue(post, "content")) }</li> 38 - } 39 - </ul> 40 - </body> 41 - </html> 42 - }
-106
pages/home_templ.go
··· 1 - // Code generated by templ - DO NOT EDIT. 2 - 3 - // templ: version: v0.2.747 4 - package pages 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/models" 12 - import "fmt" 13 - import "encoding/json" 14 - 15 - func getPostPropertyValue(post *models.Post, name string) any { 16 - props := make(map[string]any) 17 - if err := json.Unmarshal(post.Properties, &props); err != nil { 18 - panic(err) 19 - } 20 - 21 - return props[name] 22 - } 23 - 24 - func Home(profileUrl string, posts []*models.Post) templ.Component { 25 - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 26 - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 27 - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 28 - if !templ_7745c5c3_IsBuffer { 29 - defer func() { 30 - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 31 - if templ_7745c5c3_Err == nil { 32 - templ_7745c5c3_Err = templ_7745c5c3_BufErr 33 - } 34 - }() 35 - } 36 - ctx = templ.InitializeContext(ctx) 37 - templ_7745c5c3_Var1 := templ.GetChildren(ctx) 38 - if templ_7745c5c3_Var1 == nil { 39 - templ_7745c5c3_Var1 = templ.NopComponent 40 - } 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>") 56 - if templ_7745c5c3_Err != nil { 57 - return templ_7745c5c3_Err 58 - } 59 - for _, post := range posts { 60 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a href=\"") 61 - if templ_7745c5c3_Err != nil { 62 - return templ_7745c5c3_Err 63 - } 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))) 66 - if templ_7745c5c3_Err != nil { 67 - return templ_7745c5c3_Err 68 - } 69 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">") 70 - if templ_7745c5c3_Err != nil { 71 - return templ_7745c5c3_Err 72 - } 73 - var templ_7745c5c3_Var4 string 74 - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(post.ID.String()) 75 - if templ_7745c5c3_Err != nil { 76 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/home.templ`, Line: 37, Col: 82} 77 - } 78 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 79 - if templ_7745c5c3_Err != nil { 80 - return templ_7745c5c3_Err 81 - } 82 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> - ") 83 - if templ_7745c5c3_Err != nil { 84 - return templ_7745c5c3_Err 85 - } 86 - var templ_7745c5c3_Var5 string 87 - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(getPostPropertyValue(post, "content"))) 88 - if templ_7745c5c3_Err != nil { 89 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pages/home.templ`, Line: 37, Col: 142} 90 - } 91 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 92 - if templ_7745c5c3_Err != nil { 93 - return templ_7745c5c3_Err 94 - } 95 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>") 96 - if templ_7745c5c3_Err != nil { 97 - return templ_7745c5c3_Err 98 - } 99 - } 100 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></body></html>") 101 - if templ_7745c5c3_Err != nil { 102 - return templ_7745c5c3_Err 103 - } 104 - return templ_7745c5c3_Err 105 - }) 106 - }
-61
pages/post.templ
··· 1 - package pages 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 - } 42 - 43 - 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> 61 - }
-136
pages/post_templ.go
··· 1 - // Code generated by templ - DO NOT EDIT. 2 - 3 - // templ: version: v0.2.747 4 - package pages 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/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 - } 50 - 51 - func Post(post *models.Post) templ.Component { 52 - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 53 - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 54 - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 55 - if !templ_7745c5c3_IsBuffer { 56 - defer func() { 57 - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 58 - if templ_7745c5c3_Err == nil { 59 - templ_7745c5c3_Err = templ_7745c5c3_BufErr 60 - } 61 - }() 62 - } 63 - ctx = templ.InitializeContext(ctx) 64 - templ_7745c5c3_Var1 := templ.GetChildren(ctx) 65 - if templ_7745c5c3_Var1 == nil { 66 - templ_7745c5c3_Var1 = templ.NopComponent 67 - } 68 - 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...) 75 - if templ_7745c5c3_Err != nil { 76 - return templ_7745c5c3_Err 77 - } 78 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"") 79 - if templ_7745c5c3_Err != nil { 80 - return templ_7745c5c3_Err 81 - } 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: `pages/post.templ`, Line: 1, Col: 0} 86 - } 87 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 88 - if templ_7745c5c3_Err != nil { 89 - return templ_7745c5c3_Err 90 - } 91 - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><h1 class=\"p-name\">") 92 - if templ_7745c5c3_Err != nil { 93 - return templ_7745c5c3_Err 94 - } 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: `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: `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: `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>") 131 - if templ_7745c5c3_Err != nil { 132 - return templ_7745c5c3_Err 133 - } 134 - return templ_7745c5c3_Err 135 - }) 136 - }
-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: {}
+15
scripts/run.sh
··· 1 + #!/bin/bash 2 + set -e 3 + 4 + echo "Starting script" 5 + 6 + # Restore the database if it does not already exist. 7 + if [ -f /data/data.db ]; then 8 + echo "Database already exists, skipping restore" 9 + else 10 + echo "No database found, restoring from replica if exists" 11 + litestream restore -if-replica-exists /data/data.db 12 + fi 13 + 14 + # Run litestream with your app as the subprocess. 15 + exec litestream replicate -exec "/space"
+246
services/indieauth.go
··· 1 + package services 2 + 3 + import ( 4 + "context" 5 + "crypto/sha256" 6 + "crypto/subtle" 7 + "encoding/json" 8 + "errors" 9 + "net/http" 10 + "net/url" 11 + "os" 12 + "strings" 13 + "time" 14 + 15 + "github.com/puregarlic/space/html/layouts" 16 + "github.com/puregarlic/space/html/pages" 17 + "github.com/puregarlic/space/storage" 18 + 19 + "github.com/aidarkhanov/nanoid" 20 + "github.com/golang-jwt/jwt/v5" 21 + "go.hacdias.com/indielib/indieauth" 22 + ) 23 + 24 + type IndieAuth struct { 25 + ProfileURL string 26 + Server *indieauth.Server 27 + } 28 + 29 + func (i *IndieAuth) storeAuthorization(req *indieauth.AuthenticationRequest) string { 30 + code := nanoid.New() 31 + 32 + storage.AuthCache().Set(code, req, 0) 33 + 34 + return code 35 + } 36 + 37 + type CustomTokenClaims struct { 38 + Scopes []string `json:"scopes"` 39 + jwt.RegisteredClaims 40 + } 41 + 42 + type contextKey string 43 + 44 + const ( 45 + scopesContextKey contextKey = "scopes" 46 + ) 47 + 48 + func (i *IndieAuth) HandleAuthGET(w http.ResponseWriter, r *http.Request) { 49 + req, err := i.Server.ParseAuthorization(r) 50 + if err != nil { 51 + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 52 + return 53 + } 54 + 55 + app, _ := i.Server.DiscoverApplicationMetadata(r.Context(), req.ClientID) 56 + 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) 61 + } 62 + 63 + func (i *IndieAuth) HandleAuthPOST(w http.ResponseWriter, r *http.Request) { 64 + i.authorizationCodeExchange(w, r, false) 65 + } 66 + 67 + func (i *IndieAuth) HandleToken(w http.ResponseWriter, r *http.Request) { 68 + if r.Method != http.MethodPost { 69 + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 70 + return 71 + } 72 + 73 + if r.Form.Get("grant_type") == "refresh_token" { 74 + // NOTE: this server does not implement refresh tokens. 75 + // https://indieauth.spec.indieweb.org/#refresh-tokens 76 + w.WriteHeader(http.StatusNotImplemented) 77 + return 78 + } 79 + 80 + i.authorizationCodeExchange(w, r, true) 81 + } 82 + 83 + type tokenResponse struct { 84 + Me string `json:"me"` 85 + AccessToken string `json:"access_token,omitempty"` 86 + TokenType string `json:"token_type,omitempty"` 87 + Scope string `json:"scope,omitempty"` 88 + ExpiresIn int64 `json:"expires_in,omitempty"` 89 + } 90 + 91 + func (i *IndieAuth) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) { 92 + if err := r.ParseForm(); err != nil { 93 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 94 + return 95 + } 96 + 97 + // t := s.getAuthorization(r.Form.Get("code")) 98 + req, present := storage.AuthCache().GetAndDelete(r.Form.Get("code")) 99 + if !present { 100 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", "invalid authorization") 101 + return 102 + } 103 + authRequest := req.Value() 104 + 105 + err := i.Server.ValidateTokenExchange(authRequest, r) 106 + if err != nil { 107 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 108 + return 109 + } 110 + 111 + response := &tokenResponse{ 112 + Me: i.ProfileURL, 113 + } 114 + 115 + scopes := authRequest.Scopes 116 + 117 + if withToken { 118 + now := time.Now() 119 + expiresAt := now.Add(15 * time.Minute) 120 + claims := CustomTokenClaims{ 121 + scopes, 122 + jwt.RegisteredClaims{ 123 + ExpiresAt: jwt.NewNumericDate(expiresAt), 124 + IssuedAt: jwt.NewNumericDate(now), 125 + NotBefore: jwt.NewNumericDate(now), 126 + }, 127 + } 128 + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 129 + 130 + secret := os.Getenv("JWT_SECRET") 131 + jwt, err := token.SignedString([]byte(secret)) 132 + if err != nil { 133 + panic(err) 134 + } 135 + 136 + response.AccessToken = jwt 137 + response.TokenType = "Bearer" 138 + response.ExpiresIn = int64(time.Until(expiresAt).Seconds()) 139 + response.Scope = strings.Join(scopes, " ") 140 + } 141 + 142 + // An actual server may want to include the "profile" in the response if the 143 + // scope "profile" is included. 144 + SendJSON(w, http.StatusOK, response) 145 + } 146 + 147 + func (i *IndieAuth) HandleAuthApproval(w http.ResponseWriter, r *http.Request) { 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 + 158 + req, err := i.Server.ParseAuthorization(r) 159 + if err != nil { 160 + SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 161 + return 162 + } 163 + 164 + code := i.storeAuthorization(req) 165 + 166 + // Redirect to client callback. 167 + query := url.Values{} 168 + query.Set("code", code) 169 + query.Set("state", req.State) 170 + http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound) 171 + } 172 + 173 + func MustAuth(next http.Handler) http.Handler { 174 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 175 + tokenStr := r.Header.Get("Authorization") 176 + tokenStr = strings.TrimPrefix(tokenStr, "Bearer") 177 + tokenStr = strings.TrimSpace(tokenStr) 178 + 179 + if len(tokenStr) <= 0 { 180 + SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "no credentials") 181 + return 182 + } 183 + 184 + token, err := jwt.ParseWithClaims(tokenStr, &CustomTokenClaims{}, func(t *jwt.Token) (interface{}, error) { 185 + return []byte(os.Getenv("JWT_SECRET")), nil 186 + }) 187 + 188 + if err != nil { 189 + SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "invalid token") 190 + return 191 + } else if claims, ok := token.Claims.(*CustomTokenClaims); ok { 192 + ctx := context.WithValue(r.Context(), scopesContextKey, claims.Scopes) 193 + next.ServeHTTP(w, r.WithContext(ctx)) 194 + return 195 + } else { 196 + SendErrorJSON(w, http.StatusUnauthorized, "invalid_request", "malformed claims") 197 + return 198 + } 199 + }) 200 + } 201 + 202 + func MustBasicAuth(next http.Handler) http.Handler { 203 + user, ok := os.LookupEnv("ADMIN_USERNAME") 204 + if !ok { 205 + panic(errors.New("ADMIN_USERNAME is not set, cannot start")) 206 + } 207 + 208 + pass, ok := os.LookupEnv("ADMIN_PASSWORD") 209 + if !ok { 210 + panic(errors.New("ADMIN_PASSWORD is not set, cannot start")) 211 + } 212 + 213 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 214 + username, password, ok := r.BasicAuth() 215 + if ok { 216 + usernameHash := sha256.Sum256([]byte(username)) 217 + passwordHash := sha256.Sum256([]byte(password)) 218 + expectedUsernameHash := sha256.Sum256([]byte(user)) 219 + expectedPasswordHash := sha256.Sum256([]byte(pass)) 220 + 221 + usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) 222 + passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) 223 + 224 + if usernameMatch && passwordMatch { 225 + next.ServeHTTP(w, r) 226 + return 227 + } 228 + } 229 + 230 + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) 231 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 232 + }) 233 + } 234 + 235 + func SendJSON(w http.ResponseWriter, code int, data interface{}) { 236 + w.Header().Set("Content-Type", "application/json; charset=utf-8") 237 + w.WriteHeader(code) 238 + _ = json.NewEncoder(w).Encode(data) 239 + } 240 + 241 + func SendErrorJSON(w http.ResponseWriter, code int, err, errDescription string) { 242 + SendJSON(w, code, map[string]string{ 243 + "error": err, 244 + "error_description": errDescription, 245 + }) 246 + }
+270
services/micropub.go
··· 1 + package services 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "fmt" 8 + "mime/multipart" 9 + "net/http" 10 + urlpkg "net/url" 11 + "os" 12 + "reflect" 13 + "strings" 14 + 15 + "github.com/puregarlic/space/models" 16 + "github.com/puregarlic/space/storage" 17 + 18 + "github.com/aidarkhanov/nanoid" 19 + "github.com/aws/aws-sdk-go-v2/aws" 20 + "github.com/aws/aws-sdk-go-v2/service/s3" 21 + "github.com/h2non/filetype" 22 + "github.com/samber/lo" 23 + 24 + "go.hacdias.com/indielib/microformats" 25 + "go.hacdias.com/indielib/micropub" 26 + ) 27 + 28 + type Micropub struct { 29 + ProfileURL string 30 + } 31 + 32 + func postIdFromUrlPath(path string) string { 33 + return strings.TrimPrefix(path, "/posts/") 34 + } 35 + 36 + func (m *Micropub) HasScope(r *http.Request, scope string) bool { 37 + v := r.Context().Value(scopesContextKey) 38 + if scopes, ok := v.([]string); ok { 39 + for _, sc := range scopes { 40 + if sc == scope { 41 + return true 42 + } 43 + } 44 + } 45 + 46 + return false 47 + } 48 + 49 + func (m *Micropub) Source(urlStr string) (map[string]any, error) { 50 + url, err := urlpkg.Parse(urlStr) 51 + if err != nil { 52 + return nil, fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 53 + } 54 + 55 + id := postIdFromUrlPath(url.Path) 56 + post := &models.Post{} 57 + 58 + res := storage.GORM().Find(post, "id = ?", id) 59 + if res.Error != nil { 60 + panic(res.Error) 61 + } else if res.RowsAffected == 0 { 62 + return nil, micropub.ErrNotFound 63 + } 64 + 65 + return map[string]any{ 66 + "type": []string{post.Type}, 67 + "properties": post.Properties, 68 + }, nil 69 + } 70 + 71 + func (m *Micropub) SourceMany(limit, offset int) ([]map[string]any, error) { 72 + return nil, micropub.ErrNotImplemented 73 + } 74 + 75 + func (m *Micropub) HandleMediaUpload(file multipart.File, header *multipart.FileHeader) (string, error) { 76 + defer file.Close() 77 + 78 + kind, err := filetype.MatchReader(file) 79 + if _, err := file.Seek(0, 0); err != nil { 80 + return "", fmt.Errorf("%w: %w", errors.New("failed to reset cursor"), err) 81 + } 82 + 83 + if err != nil { 84 + return "", fmt.Errorf("%w: %w", errors.New("failed to upload"), err) 85 + } 86 + 87 + key := fmt.Sprintf("media/%s.%s", nanoid.New(), kind.Extension) 88 + _, err = storage.S3().PutObject(context.TODO(), &s3.PutObjectInput{ 89 + Bucket: aws.String(os.Getenv("AWS_S3_BUCKET_NAME")), 90 + Key: &key, 91 + Body: file, 92 + }) 93 + if err != nil { 94 + return "", fmt.Errorf("%w: %w", errors.New("failed to upload"), err) 95 + } 96 + 97 + return m.ProfileURL + key, nil 98 + } 99 + 100 + func (m *Micropub) Create(req *micropub.Request) (string, error) { 101 + props, err := json.Marshal(req.Properties) 102 + if err != nil { 103 + return "", err 104 + } 105 + 106 + mfType, _ := microformats.DiscoverType(map[string]any{ 107 + "type": req.Type, 108 + "properties": req.Properties, 109 + }) 110 + 111 + post := &models.Post{ 112 + ID: models.NewULID(), 113 + Type: req.Type, 114 + MicroformatType: mfType, 115 + Properties: props, 116 + } 117 + 118 + res := storage.GORM().Create(post) 119 + if res.Error != nil { 120 + return "", res.Error 121 + } 122 + 123 + return m.ProfileURL + "posts/" + post.ID.String(), nil 124 + } 125 + 126 + func (m *Micropub) Update(req *micropub.Request) (string, error) { 127 + url, err := urlpkg.Parse(req.URL) 128 + if err != nil { 129 + return "", fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 130 + } 131 + 132 + id := postIdFromUrlPath(url.Path) 133 + post := &models.Post{} 134 + 135 + res := storage.GORM().Find(post, "id = ?", id) 136 + if res.Error != nil { 137 + panic(res.Error) 138 + } else if res.RowsAffected != 1 { 139 + return "", micropub.ErrNotFound 140 + } 141 + 142 + newProps, err := updateProperties(json.RawMessage(post.Properties), req) 143 + if err != nil { 144 + panic(err) 145 + } 146 + 147 + post.Properties = newProps 148 + 149 + storage.GORM().Save(post) 150 + 151 + return url.String(), nil 152 + } 153 + 154 + func (m *Micropub) Delete(urlStr string) error { 155 + url, err := urlpkg.Parse(urlStr) 156 + if err != nil { 157 + return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 158 + } 159 + 160 + id := postIdFromUrlPath(url.Path) 161 + 162 + res := storage.GORM().Delete(&models.Post{}, "id = ?", id) 163 + if res.Error != nil { 164 + panic(res.Error) 165 + } else if res.RowsAffected == 0 { 166 + return fmt.Errorf("%w: %w", micropub.ErrNotFound, err) 167 + } 168 + 169 + return nil 170 + } 171 + 172 + func (m *Micropub) Undelete(urlStr string) error { 173 + url, err := urlpkg.Parse(urlStr) 174 + if err != nil { 175 + return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err) 176 + } 177 + 178 + id := postIdFromUrlPath(url.Path) 179 + res := storage.GORM().Unscoped().Model(&models.Post{}).Where("id = ?", id).Update("deleted_at", nil) 180 + if res.Error != nil { 181 + return res.Error 182 + } else if res.RowsAffected != 1 { 183 + return micropub.ErrNotFound 184 + } 185 + 186 + return nil 187 + } 188 + 189 + // updateProperties applies the updates (additions, deletions, replacements) 190 + // in the given [micropub.Request] to a set of existing microformats properties. 191 + func updateProperties(props json.RawMessage, req *micropub.Request) ([]byte, error) { 192 + properties := make(map[string][]any) 193 + if err := json.Unmarshal(props, &properties); err != nil { 194 + panic(err) 195 + } 196 + 197 + if req.Updates.Replace != nil { 198 + for key, value := range req.Updates.Replace { 199 + properties[key] = value 200 + } 201 + } 202 + 203 + if req.Updates.Add != nil { 204 + for key, value := range req.Updates.Add { 205 + switch key { 206 + case "name": 207 + return nil, errors.New("cannot add a new name") 208 + case "content": 209 + return nil, errors.New("cannot add content") 210 + default: 211 + if key == "published" { 212 + if _, ok := properties["published"]; ok { 213 + return nil, errors.New("cannot replace published through add method") 214 + } 215 + } 216 + 217 + if _, ok := properties[key]; !ok { 218 + properties[key] = []any{} 219 + } 220 + 221 + properties[key] = append(properties[key], value...) 222 + } 223 + } 224 + } 225 + 226 + if req.Updates.Delete != nil { 227 + if reflect.TypeOf(req.Updates.Delete).Kind() == reflect.Slice { 228 + toDelete, ok := req.Updates.Delete.([]any) 229 + if !ok { 230 + return nil, errors.New("invalid delete array") 231 + } 232 + 233 + for _, key := range toDelete { 234 + delete(properties, fmt.Sprint(key)) 235 + } 236 + } else { 237 + toDelete, ok := req.Updates.Delete.(map[string]any) 238 + if !ok { 239 + return nil, fmt.Errorf("invalid delete object: expected map[string]any, got: %s", reflect.TypeOf(req.Updates.Delete)) 240 + } 241 + 242 + for key, v := range toDelete { 243 + value, ok := v.([]any) 244 + if !ok { 245 + return nil, fmt.Errorf("invalid value: expected []any, got: %s", reflect.TypeOf(value)) 246 + } 247 + 248 + if _, ok := properties[key]; !ok { 249 + properties[key] = []any{} 250 + } 251 + 252 + properties[key] = lo.Filter(properties[key], func(ss any, _ int) bool { 253 + for _, s := range value { 254 + if s == ss { 255 + return false 256 + } 257 + } 258 + return true 259 + }) 260 + } 261 + } 262 + } 263 + 264 + propJson, err := json.Marshal(&properties) 265 + if err != nil { 266 + panic(err) 267 + } 268 + 269 + return propJson, nil 270 + }
+47
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 + var nonceCache *ttlcache.Cache[string, string] 12 + 13 + func CleanupCaches() { 14 + AuthCache().Stop() 15 + } 16 + 17 + func AuthCache() *ttlcache.Cache[string, *indieauth.AuthenticationRequest] { 18 + if authCache != nil { 19 + return authCache 20 + } 21 + 22 + cache := ttlcache.New( 23 + ttlcache.WithTTL[string, *indieauth.AuthenticationRequest](10 * time.Minute), 24 + ) 25 + 26 + go cache.Start() 27 + 28 + authCache = cache 29 + 30 + return cache 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 + }
+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 + }
+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 + }
+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 + }
-11
tailwind.config.js
··· 1 - /** @type {import('tailwindcss').Config} */ 2 - module.exports = { 3 - content: [ 4 - "./**/*.{html,templ}" 5 - ], 6 - theme: { 7 - extend: {}, 8 - }, 9 - plugins: [], 10 - } 11 -