+7
.dockerignore
+7
.dockerignore
+17
.env.example
+17
.env.example
···
1
+
# Port for the server to listen on.
2
+
# Change this to something you can tunnel during local development
3
+
PORT=5050
4
+
5
+
# Root URL, with protocol, where your server is accessible.
6
+
PROFILE_URL="http://localhost/"
7
+
8
+
ADMIN_USERNAME="change"
9
+
ADMIN_PASSWORD="me"
10
+
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
.gitattributes
···
1
+
*.sh text eol=lf
+18
.github/workflows/fly-deploy.yml
+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 }}
+5
-1
.gitignore
+5
-1
.gitignore
+47
README.md
+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
+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
+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
+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
+8
config/litestream.yml
+10
config/main.css
+10
config/main.css
+33
config/tailwind.config.ts
+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;
-75
db/storage.go
-75
db/storage.go
···
1
-
package db
2
-
3
-
import (
4
-
"log"
5
-
"time"
6
-
7
-
"github.com/jellydator/ttlcache/v3"
8
-
"github.com/ostafen/clover/v2"
9
-
"go.hacdias.com/indielib/indieauth"
10
-
)
11
-
12
-
type Storage struct {
13
-
Docs *clover.DB
14
-
Authorization *ttlcache.Cache[string, *indieauth.AuthenticationRequest]
15
-
}
16
-
17
-
type CollectionName string
18
-
19
-
var (
20
-
PostCollection CollectionName = "posts"
21
-
)
22
-
23
-
func NewStorage() *Storage {
24
-
c, err := clover.Open("data/docs")
25
-
if err != nil {
26
-
log.Fatal(err)
27
-
}
28
-
29
-
cache := ttlcache.New[string, *indieauth.AuthenticationRequest](
30
-
ttlcache.WithTTL[string, *indieauth.AuthenticationRequest](10 * time.Minute),
31
-
)
32
-
33
-
go cache.Start()
34
-
35
-
store := &Storage{
36
-
Docs: c,
37
-
Authorization: cache,
38
-
}
39
-
40
-
store.SetupClover()
41
-
42
-
return store
43
-
}
44
-
45
-
func (db *Storage) SetupClover() {
46
-
if ok, err := db.Docs.HasCollection(string(PostCollection)); err != nil {
47
-
panic(err)
48
-
} else if !ok {
49
-
err := db.Docs.CreateCollection(string(PostCollection))
50
-
if err != nil {
51
-
panic(err)
52
-
}
53
-
}
54
-
55
-
if ok, err := db.Docs.HasIndex(string(PostCollection), "createdAt"); err != nil {
56
-
panic(err)
57
-
} else if !ok {
58
-
err := db.Docs.CreateIndex(string(PostCollection), "createdAt")
59
-
if err != nil {
60
-
panic(err)
61
-
}
62
-
}
63
-
}
64
-
65
-
func (db *Storage) Cleanup() {
66
-
if err := db.Docs.Close(); err != nil {
67
-
panic(err)
68
-
}
69
-
70
-
db.Authorization.Stop()
71
-
}
72
-
73
-
func (db *Storage) SetupTokenCache() {
74
-
75
-
}
-4
gen.go
-4
gen.go
+40
-20
go.mod
+40
-20
go.mod
···
3
3
go 1.22.5
4
4
5
5
require (
6
+
codeberg.org/gruf/go-ulid v1.1.0
6
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
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
7
13
github.com/jellydator/ttlcache/v3 v3.2.0
8
14
github.com/joho/godotenv v1.5.1
9
-
github.com/ostafen/clover/v2 v2.0.0-alpha.3
10
15
go.hacdias.com/indielib v0.3.1
16
+
gorm.io/datatypes v1.2.1
17
+
gorm.io/gorm v1.25.11
11
18
)
12
19
13
20
require (
14
-
github.com/cespare/xxhash v1.1.0 // indirect
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
15
37
github.com/cespare/xxhash/v2 v2.3.0 // indirect
16
-
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
17
-
github.com/dgraph-io/ristretto v0.1.0 // indirect
18
-
github.com/dustin/go-humanize v1.0.0 // indirect
19
-
github.com/gogo/protobuf v1.3.2 // indirect
20
-
github.com/golang/glog v1.0.0 // indirect
21
-
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
22
-
github.com/golang/protobuf v1.5.2 // indirect
23
-
github.com/golang/snappy v0.0.4 // indirect
24
-
github.com/google/flatbuffers v2.0.6+incompatible // indirect
25
-
github.com/google/orderedcode v0.0.1 // indirect
26
-
github.com/klauspost/compress v1.15.9 // indirect
27
-
github.com/pkg/errors v0.9.1 // indirect
28
-
github.com/satori/go.uuid v1.2.0 // indirect
29
-
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
30
-
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
31
-
go.etcd.io/bbolt v1.3.6 // indirect
32
-
go.opencensus.io v0.23.0 // indirect
38
+
github.com/dlclark/regexp2 v1.11.0 // indirect
39
+
github.com/dustin/go-humanize v1.0.1 // indirect
40
+
github.com/glebarez/go-sqlite v1.21.2 // indirect
41
+
github.com/go-sql-driver/mysql v1.8.1 // indirect
42
+
github.com/google/uuid v1.3.0 // indirect
43
+
github.com/jinzhu/inflection v1.0.0 // indirect
44
+
github.com/jinzhu/now v1.1.5 // indirect
45
+
github.com/mattn/go-isatty v0.0.20 // indirect
46
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
33
47
golang.org/x/sync v0.7.0 // indirect
34
48
golang.org/x/sys v0.22.0 // indirect
35
49
golang.org/x/text v0.16.0 // indirect
36
-
google.golang.org/protobuf v1.28.1 // indirect
50
+
gorm.io/driver/mysql v1.5.6 // indirect
51
+
modernc.org/libc v1.22.5 // indirect
52
+
modernc.org/mathutil v1.5.0 // indirect
53
+
modernc.org/memory v1.5.0 // indirect
54
+
modernc.org/sqlite v1.23.1 // indirect
37
55
)
38
56
39
57
require (
40
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
41
61
github.com/go-chi/chi/v5 v5.1.0
42
62
github.com/go-chi/httprate v0.12.0
43
63
github.com/golang-jwt/jwt/v5 v5.2.1
+116
-208
go.sum
+116
-208
go.sum
···
1
-
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2
-
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3
-
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
4
-
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
1
+
codeberg.org/gruf/go-ulid v1.1.0 h1:run+0r3hQqF1G7Am1oAt/msoK+FPKnlcQ6sc/biXWqw=
2
+
codeberg.org/gruf/go-ulid v1.1.0/go.mod h1:dVNsZJpVTge8+jfBTjAMpnscYSiCqk+g32ZgtorCAMs=
3
+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
4
+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
5
5
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
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=
10
-
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
11
-
github.com/brianvoe/gofakeit/v6 v6.17.0 h1:obbQTJeHfktJtiZzq0Q1bEpsNUs+yHrYlPVWt7BtmJ4=
12
-
github.com/brianvoe/gofakeit/v6 v6.17.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
13
-
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
14
-
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
15
-
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
16
-
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
17
-
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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=
18
52
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
19
53
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
20
-
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
21
-
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
22
-
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
23
-
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
24
-
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
25
-
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
26
-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
27
54
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
28
55
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
29
-
github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8=
30
-
github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=
31
-
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
32
-
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
33
-
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
34
-
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
35
-
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
36
-
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
37
-
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
38
-
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
39
-
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
40
-
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
41
-
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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=
58
+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
59
+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
60
+
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
61
+
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
62
+
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
63
+
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
42
64
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
43
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=
44
68
github.com/go-chi/httprate v0.12.0 h1:08D/te3pOTJe5+VAZTQrHxwdsH2NyliiUoRD1naKaMg=
45
69
github.com/go-chi/httprate v0.12.0/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0=
46
-
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
47
-
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
70
+
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
71
+
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
72
+
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
48
73
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
49
74
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
50
-
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
51
-
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
52
-
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
53
-
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
54
-
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
55
-
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
56
-
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
57
-
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
58
-
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
59
-
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
60
-
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
61
-
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
62
-
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
63
-
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
64
-
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
65
-
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
66
-
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
67
-
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
68
-
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
69
-
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
70
-
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
71
-
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
72
-
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
73
-
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
74
-
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
75
-
github.com/google/flatbuffers v2.0.6+incompatible h1:XHFReMv7nFFusa+CEokzWbzaYocKXI6C7hdU5Kgh9Lw=
76
-
github.com/google/flatbuffers v2.0.6+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
77
-
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
78
-
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
79
-
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
80
-
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
81
-
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
82
-
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
83
-
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
84
-
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
75
+
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
76
+
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
77
+
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
78
+
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
85
79
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
86
80
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
87
81
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
88
-
github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us=
89
-
github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20=
90
-
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
91
-
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
92
-
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
82
+
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
83
+
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
84
+
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
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=
90
+
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
91
+
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
92
+
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
93
+
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
94
+
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
95
+
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
96
+
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
97
+
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
93
98
github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE=
94
99
github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
100
+
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
101
+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
102
+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
103
+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
95
104
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
96
105
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
97
-
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
98
-
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
99
-
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
100
-
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
101
-
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
102
-
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
103
-
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
104
-
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
105
-
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
106
-
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
107
-
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
108
-
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
109
-
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
110
-
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
111
-
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
112
-
github.com/ostafen/clover/v2 v2.0.0-alpha.3 h1:fXC7tVHQkUPFlxlj/kD98h0ngrTpIeJymaxVIqDzw3Q=
113
-
github.com/ostafen/clover/v2 v2.0.0-alpha.3/go.mod h1:5YCDt+wJDUNN1uSXE5csxSQBuJrNjidkOkJTXWuNhDY=
114
-
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
115
-
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
116
-
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
106
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
107
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
108
+
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
109
+
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
110
+
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
111
+
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
117
112
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
118
113
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
119
-
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
120
-
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
114
+
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
115
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
116
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
121
117
github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ=
122
118
github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
123
-
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
124
-
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
125
-
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
126
-
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
127
-
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
128
-
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
129
-
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
130
-
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
131
-
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
132
-
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
133
-
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
134
-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
135
119
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
136
120
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
137
-
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
138
-
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
139
-
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
140
-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
141
121
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
142
122
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
143
-
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
144
-
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
145
-
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
146
-
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
147
-
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
148
123
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
149
-
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
150
-
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
151
-
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
152
-
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
153
-
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
154
124
go.hacdias.com/indielib v0.3.1 h1:t6IUp2lfQBa3baXcBN9S/A4fBq4vzMWVbfCQgPcDpy8=
155
125
go.hacdias.com/indielib v0.3.1/go.mod h1:ushJ07W6LxAbZWhyqXzQQxXkalPkZo6cGz5Uj2wOdb4=
156
-
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
157
-
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
158
-
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
159
126
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
160
127
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
161
-
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
162
-
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
163
-
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
164
-
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
165
-
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
166
-
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
167
-
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
168
-
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
169
-
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
170
-
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
171
-
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
172
-
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
173
-
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
174
-
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
175
-
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
176
-
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
177
-
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
178
-
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
179
-
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
128
+
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
129
+
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
180
130
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
181
131
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
182
-
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
183
132
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
184
133
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
185
-
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
186
134
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
187
135
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
188
-
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
189
-
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
190
-
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
191
-
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
192
-
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
193
-
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
194
136
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
195
137
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
196
-
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
197
-
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
198
-
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
199
-
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
200
-
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
201
-
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
202
-
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
203
138
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
204
-
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
205
139
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
206
-
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
207
-
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
140
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
208
141
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
209
142
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
210
143
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
211
-
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
212
-
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
213
-
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
214
144
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
215
-
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
216
145
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
217
146
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
218
147
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
219
-
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
220
-
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
221
-
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
222
-
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
223
-
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
224
-
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
225
-
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
226
-
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
227
-
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
228
148
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
229
-
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
230
-
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
231
-
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
232
-
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
233
-
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
234
-
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
235
-
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
236
-
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
237
-
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
238
-
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
239
-
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
240
-
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
241
-
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
242
-
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
243
-
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
244
-
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
245
-
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
246
-
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
247
-
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
248
-
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
249
-
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
250
-
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
251
-
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
252
-
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
253
-
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
254
-
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
255
-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
256
-
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
257
-
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
258
-
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
259
-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
260
149
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
261
150
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
262
-
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
263
-
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
151
+
gorm.io/datatypes v1.2.1 h1:r+g0bk4LPCW2v4+Ls7aeNgGme7JYdNDQ2VtvlNUfBh0=
152
+
gorm.io/datatypes v1.2.1/go.mod h1:hYK6OTb/1x+m96PgoZZq10UXJ6RvEBb9kRDQ2yyhzGs=
153
+
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
154
+
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
155
+
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
156
+
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
157
+
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
158
+
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
159
+
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
160
+
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
161
+
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
162
+
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
163
+
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
164
+
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
165
+
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
166
+
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
167
+
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
168
+
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
169
+
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
170
+
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
171
+
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
264
172
willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e h1:TRIOwo0NxN4KVSgYlYmiQktd9I96YgZ3942/JVzhwTM=
265
173
willnorris.com/go/microformats v1.2.1-0.20240301064101-b5d1b9d2120e/go.mod h1:zzo0hFA/E/nl1ZAjXiXA7KCKwCTdgBU+7HXltGgHeGA=
266
174
willnorris.com/go/webmention v0.0.0-20220108183051-4a23794272f0 h1:V5+O+YZHchEwu6ZmPcqT1dQ+mHgE356Q+w9SVOQ+QZg=
+35
handlers/media.go
+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
+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
+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
+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
+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&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
+9
html/components/posts/note.templ
+50
html/components/posts/note_templ.go
+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
+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
+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
+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">•</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
+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\">•</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
+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
+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
+
}
+22
html/layouts/default.templ
+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
+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
+
}
+63
html/pages/auth.templ
+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
+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
+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
+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
+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
+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
-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
-
}
+44
-137
main.go
+44
-137
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
-
"flag"
7
+
"os"
6
8
"time"
7
9
8
10
"log"
9
11
"net/http"
10
12
"strconv"
11
13
12
-
"github.com/ostafen/clover/v2/query"
14
+
"github.com/puregarlic/space/handlers"
15
+
"github.com/puregarlic/space/models"
16
+
"github.com/puregarlic/space/storage"
13
17
14
-
"github.com/puregarlic/space/db"
15
-
"github.com/puregarlic/space/pages"
16
-
"github.com/puregarlic/space/types"
17
-
18
-
"github.com/a-h/templ"
19
18
"github.com/go-chi/chi/v5"
20
19
"github.com/go-chi/chi/v5/middleware"
20
+
"github.com/go-chi/cors"
21
21
"github.com/go-chi/httprate"
22
22
23
23
"go.hacdias.com/indielib/indieauth"
24
-
"go.hacdias.com/indielib/microformats"
25
-
"go.hacdias.com/indielib/micropub"
26
24
27
25
_ "github.com/joho/godotenv/autoload"
28
26
)
29
27
30
28
func main() {
31
-
// Setup flags.
32
-
portPtr := flag.Int("port", 80, "port to listen on")
33
-
addressPtr := flag.String("profile", "http://localhost/", "client URL and front facing address to listen on")
34
-
flag.Parse()
35
-
36
-
profileURL := *addressPtr
29
+
port, profileURL := validateRuntimeConfiguration()
30
+
defer storage.CleanupCaches()
37
31
38
-
// Validate the given Client ID before starting the HTTP server.
39
-
err := indieauth.IsValidProfileURL(profileURL)
40
-
if err != nil {
41
-
log.Fatal(err)
42
-
}
43
-
44
-
// Setup storage handlers
45
-
store := db.NewStorage()
46
-
defer store.Cleanup()
47
-
48
-
// Create a new client.
49
-
s := &server{
50
-
profileURL: profileURL,
51
-
ias: indieauth.NewServer(true, nil),
52
-
db: store,
53
-
}
32
+
storage.GORM().AutoMigrate(&models.Post{})
54
33
55
34
r := chi.NewRouter()
56
35
···
58
37
r.Use(middleware.RealIP)
59
38
r.Use(middleware.Logger)
60
39
r.Use(middleware.Recoverer)
61
-
62
40
r.Use(httprate.LimitByIP(100, 1*time.Minute))
63
41
64
-
// Static resources
65
-
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)
66
45
67
-
// Pages
68
-
r.Get("/", s.serveHomeTemplate)
69
-
r.Get("/posts/{slug}", s.servePostTemplate)
46
+
// Content pages
47
+
r.Get("/posts/{slug}", handlers.ServePostPage)
70
48
71
-
// IndieAuth handlers
72
-
r.Group(func(r chi.Router) {
73
-
r.Post("/token", s.tokenHandler)
74
-
r.Post("/authorization", s.authorizationPostHandler)
75
-
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)
76
55
77
-
// User authentication portal
78
-
r.With(s.mustBasicAuth).Get("/authorization", s.authorizationGetHandler)
79
-
})
80
-
81
-
// Micropub handler
82
-
r.Route("/micropub", func(r chi.Router) {
83
-
r.Use(s.mustAuth)
84
-
r.Get("/", s.serveMicropub)
85
-
r.Post("/", s.serveMicropub)
86
-
})
56
+
// Service handlers
57
+
handlers.AttachIndieAuth(r, "/authorization", profileURL)
58
+
handlers.AttachMicropub(r, "/micropub", profileURL)
87
59
88
60
// Start it!
89
-
log.Printf("Listening on http://localhost:%d", *portPtr)
61
+
log.Printf("Listening on http://localhost:%d", port)
90
62
log.Printf("Listening on %s", profileURL)
91
-
if err := http.ListenAndServe(":"+strconv.Itoa(*portPtr), r); err != nil {
63
+
if err := http.ListenAndServe(":"+strconv.Itoa(port), r); err != nil {
92
64
log.Fatal(err)
93
65
}
94
66
}
95
67
96
-
type server struct {
97
-
profileURL string
98
-
ias *indieauth.Server
99
-
db *db.Storage
100
-
}
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
+
}
101
77
102
-
func (s *server) serveHomeTemplate(w http.ResponseWriter, r *http.Request) {
103
-
q := query.NewQuery(
104
-
string(db.PostCollection),
105
-
).Sort(query.SortOption{
106
-
Field: "createdAt",
107
-
Direction: -1,
108
-
}).Limit(10)
109
-
110
-
docs, err := s.db.Docs.FindAll(q)
111
-
if err != nil {
112
-
httpError(w, http.StatusInternalServerError)
113
-
panic(err)
78
+
port = portInt
114
79
}
115
80
116
-
posts := make([]*types.Post, len(docs))
117
-
for i, doc := range docs {
118
-
id := doc.ObjectId()
119
-
post := &types.Post{
120
-
ID: id,
121
-
}
122
-
123
-
if err := doc.Unmarshal(post); err != nil {
124
-
httpError(w, http.StatusInternalServerError)
125
-
panic(err)
126
-
}
127
-
128
-
post.ID = id
129
-
130
-
posts[i] = post
81
+
profileURL, ok := os.LookupEnv("PROFILE_URL")
82
+
if !ok {
83
+
profileURL = "http://localhost/"
131
84
}
132
85
133
-
templ.Handler(pages.Home(s.profileURL, posts)).ServeHTTP(w, r)
134
-
}
135
-
136
-
func (s *server) servePostTemplate(w http.ResponseWriter, r *http.Request) {
137
-
id := chi.URLParam(r, "slug")
138
-
post := &types.Post{}
139
-
140
-
doc, err := s.db.Docs.FindById(string(db.PostCollection), id)
86
+
// Validate the given Client ID before starting the HTTP server.
87
+
err := indieauth.IsValidProfileURL(profileURL)
141
88
if err != nil {
142
-
httpError(w, http.StatusInternalServerError)
143
-
return
144
-
} else if doc == nil {
145
-
httpError(w, http.StatusNotFound)
146
-
return
89
+
log.Fatal(err)
147
90
}
148
91
149
-
if err := doc.Unmarshal(post); err != nil {
150
-
httpError(w, http.StatusInternalServerError)
151
-
return
152
-
}
153
-
154
-
templ.Handler(pages.Post(post)).ServeHTTP(w, r)
155
-
}
156
-
157
-
func (s *server) serveMicropub(w http.ResponseWriter, r *http.Request) {
158
-
micropub.NewHandler(
159
-
µpubImplementation{s},
160
-
micropub.WithGetPostTypes(func() []micropub.PostType {
161
-
return []micropub.PostType{
162
-
{
163
-
Name: "Post",
164
-
Type: string(microformats.TypeNote),
165
-
},
166
-
}
167
-
}),
168
-
).ServeHTTP(w, r)
169
-
}
170
-
171
-
func httpError(w http.ResponseWriter, status int) {
172
-
http.Error(w, http.StatusText(status), status)
173
-
}
174
-
175
-
func serveJSON(w http.ResponseWriter, code int, data interface{}) {
176
-
w.Header().Set("Content-Type", "application/json; charset=utf-8")
177
-
w.WriteHeader(code)
178
-
_ = json.NewEncoder(w).Encode(data)
179
-
}
180
-
181
-
func serveErrorJSON(w http.ResponseWriter, code int, err, errDescription string) {
182
-
serveJSON(w, code, map[string]string{
183
-
"error": err,
184
-
"error_description": errDescription,
185
-
})
92
+
return port, profileURL
186
93
}
-212
micropub.go
-212
micropub.go
···
1
-
package main
2
-
3
-
import (
4
-
"errors"
5
-
"fmt"
6
-
"net/http"
7
-
urlpkg "net/url"
8
-
"reflect"
9
-
"strings"
10
-
"time"
11
-
12
-
"github.com/ostafen/clover/v2/document"
13
-
"github.com/puregarlic/space/db"
14
-
"github.com/puregarlic/space/types"
15
-
"github.com/samber/lo"
16
-
17
-
"go.hacdias.com/indielib/micropub"
18
-
)
19
-
20
-
type micropubImplementation struct {
21
-
*server
22
-
}
23
-
24
-
func postIdFromUrlPath(path string) string {
25
-
return strings.TrimPrefix(path, "/posts/")
26
-
}
27
-
28
-
func (s *micropubImplementation) HasScope(r *http.Request, scope string) bool {
29
-
v := r.Context().Value(scopesContextKey)
30
-
if scopes, ok := v.([]string); ok {
31
-
for _, sc := range scopes {
32
-
if sc == scope {
33
-
return true
34
-
}
35
-
}
36
-
}
37
-
38
-
return false
39
-
}
40
-
41
-
func (s *micropubImplementation) Source(urlStr string) (map[string]any, error) {
42
-
url, err := urlpkg.Parse(urlStr)
43
-
if err != nil {
44
-
return nil, fmt.Errorf("%w: %w", micropub.ErrBadRequest, err)
45
-
}
46
-
47
-
id := postIdFromUrlPath(url.Path)
48
-
post := &types.Post{}
49
-
doc, err := s.server.db.Docs.FindById(string(db.PostCollection), id)
50
-
if err != nil {
51
-
panic(err)
52
-
} else if doc == nil {
53
-
return nil, micropub.ErrNotFound
54
-
}
55
-
56
-
if err := doc.Unmarshal(post); err != nil {
57
-
panic(err)
58
-
}
59
-
60
-
return map[string]any{
61
-
"type": []string{post.Type},
62
-
"properties": post.Properties,
63
-
}, nil
64
-
}
65
-
66
-
func (s *micropubImplementation) SourceMany(limit, offset int) ([]map[string]any, error) {
67
-
return nil, micropub.ErrNotImplemented
68
-
}
69
-
70
-
func (s *micropubImplementation) Create(req *micropub.Request) (string, error) {
71
-
post := types.Post{
72
-
Type: req.Type,
73
-
Properties: req.Properties,
74
-
CreatedAt: time.Now().Unix(),
75
-
}
76
-
doc := document.NewDocumentOf(post)
77
-
if doc == nil {
78
-
return "", errors.New("Could not marshal post to Clover document")
79
-
}
80
-
81
-
id, err := s.server.db.Docs.InsertOne(string(db.PostCollection), doc)
82
-
if err != nil {
83
-
return "", err
84
-
}
85
-
86
-
return s.profileURL + "posts/" + id, nil
87
-
}
88
-
89
-
func (s *micropubImplementation) Update(req *micropub.Request) (string, error) {
90
-
url, err := urlpkg.Parse(req.URL)
91
-
if err != nil {
92
-
return "", fmt.Errorf("%w: %w", micropub.ErrBadRequest, err)
93
-
}
94
-
95
-
id := postIdFromUrlPath(url.Path)
96
-
97
-
if err := s.server.db.Docs.UpdateById(
98
-
string(db.PostCollection),
99
-
id,
100
-
func(doc *document.Document) *document.Document {
101
-
post := &types.Post{}
102
-
if err := doc.Unmarshal(post); err != nil {
103
-
panic(err)
104
-
}
105
-
106
-
props, err := updateProperties(post.Properties, req)
107
-
if err != nil {
108
-
panic(err)
109
-
}
110
-
111
-
doc.Set("properties", props)
112
-
113
-
return doc
114
-
},
115
-
); err != nil {
116
-
return "", fmt.Errorf("%w: %w", micropub.ErrBadRequest, err)
117
-
}
118
-
119
-
return s.profileURL + url.Path, nil
120
-
}
121
-
122
-
func (s *micropubImplementation) Delete(urlStr string) error {
123
-
url, err := urlpkg.Parse(urlStr)
124
-
if err != nil {
125
-
return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err)
126
-
}
127
-
128
-
id := postIdFromUrlPath(url.Path)
129
-
130
-
if err := s.server.db.Docs.DeleteById(string(db.PostCollection), id); err != nil {
131
-
return fmt.Errorf("%w: %w", micropub.ErrBadRequest, err)
132
-
}
133
-
134
-
return nil
135
-
}
136
-
137
-
func (s *micropubImplementation) Undelete(url string) error {
138
-
return micropub.ErrNotImplemented
139
-
}
140
-
141
-
// updateProperties applies the updates (additions, deletions, replacements)
142
-
// in the given [micropub.Request] to a set of existing microformats properties.
143
-
func updateProperties(properties map[string][]any, req *micropub.Request) (map[string][]any, error) {
144
-
if req.Updates.Replace != nil {
145
-
for key, value := range req.Updates.Replace {
146
-
properties[key] = value
147
-
}
148
-
}
149
-
150
-
if req.Updates.Add != nil {
151
-
for key, value := range req.Updates.Add {
152
-
switch key {
153
-
case "name":
154
-
return nil, errors.New("cannot add a new name")
155
-
case "content":
156
-
return nil, errors.New("cannot add content")
157
-
default:
158
-
if key == "published" {
159
-
if _, ok := properties["published"]; ok {
160
-
return nil, errors.New("cannot replace published through add method")
161
-
}
162
-
}
163
-
164
-
if _, ok := properties[key]; !ok {
165
-
properties[key] = []any{}
166
-
}
167
-
168
-
properties[key] = append(properties[key], value...)
169
-
}
170
-
}
171
-
}
172
-
173
-
if req.Updates.Delete != nil {
174
-
if reflect.TypeOf(req.Updates.Delete).Kind() == reflect.Slice {
175
-
toDelete, ok := req.Updates.Delete.([]any)
176
-
if !ok {
177
-
return nil, errors.New("invalid delete array")
178
-
}
179
-
180
-
for _, key := range toDelete {
181
-
delete(properties, fmt.Sprint(key))
182
-
}
183
-
} else {
184
-
toDelete, ok := req.Updates.Delete.(map[string]any)
185
-
if !ok {
186
-
return nil, fmt.Errorf("invalid delete object: expected map[string]any, got: %s", reflect.TypeOf(req.Updates.Delete))
187
-
}
188
-
189
-
for key, v := range toDelete {
190
-
value, ok := v.([]any)
191
-
if !ok {
192
-
return nil, fmt.Errorf("invalid value: expected []any, got: %s", reflect.TypeOf(value))
193
-
}
194
-
195
-
if _, ok := properties[key]; !ok {
196
-
properties[key] = []any{}
197
-
}
198
-
199
-
properties[key] = lo.Filter(properties[key], func(ss any, _ int) bool {
200
-
for _, s := range value {
201
-
if s == ss {
202
-
return false
203
-
}
204
-
}
205
-
return true
206
-
})
207
-
}
208
-
}
209
-
}
210
-
211
-
return properties, nil
212
-
}
+25
models/post.go
+25
models/post.go
···
1
+
package models
2
+
3
+
import (
4
+
"time"
5
+
6
+
"go.hacdias.com/indielib/microformats"
7
+
"gorm.io/datatypes"
8
+
"gorm.io/gorm"
9
+
)
10
+
11
+
type Post struct {
12
+
ID ULID `gorm:"primaryKey;unique"`
13
+
14
+
Type string
15
+
MicroformatType microformats.Type
16
+
Properties datatypes.JSON
17
+
18
+
CreatedAt time.Time
19
+
UpdatedAt time.Time
20
+
DeletedAt gorm.DeletedAt `gorm:"index"`
21
+
}
22
+
23
+
func (p *Post) Timestamp() string {
24
+
return p.CreatedAt.Format("01/02/2006 at 3:04 PM")
25
+
}
+80
models/ulid.go
+80
models/ulid.go
···
1
+
package models
2
+
3
+
import (
4
+
"database/sql/driver"
5
+
6
+
"codeberg.org/gruf/go-ulid"
7
+
"gorm.io/gorm"
8
+
"gorm.io/gorm/schema"
9
+
)
10
+
11
+
type ULID ulid.ULID
12
+
13
+
func NewULID() ULID {
14
+
return ULID(ulid.MustNew())
15
+
}
16
+
17
+
func (ULID) GormDataType() string {
18
+
return "string"
19
+
}
20
+
21
+
func (ULID) GormDBDataType(db *gorm.DB, field *schema.Field) string {
22
+
switch db.Dialector.Name() {
23
+
case "mysql":
24
+
return "LONGTEXT"
25
+
case "postgres":
26
+
return "UUID"
27
+
case "sqlserver":
28
+
return "NVARCHAR"
29
+
case "sqlite":
30
+
return "TEXT"
31
+
default:
32
+
return ""
33
+
}
34
+
}
35
+
36
+
func (u *ULID) Scan(value interface{}) error {
37
+
var result ulid.ULID
38
+
if err := result.Scan(value); err != nil {
39
+
return err
40
+
}
41
+
*u = ULID(result)
42
+
return nil
43
+
}
44
+
45
+
func (u ULID) Value() (driver.Value, error) {
46
+
return ulid.ULID(u).String(), nil
47
+
}
48
+
49
+
func (u ULID) String() string {
50
+
return ulid.ULID(u).String()
51
+
}
52
+
53
+
func (u ULID) Equals(other ULID) bool {
54
+
return u.String() == other.String()
55
+
}
56
+
57
+
func (u ULID) Length() int {
58
+
return len(u.String())
59
+
}
60
+
61
+
func (u ULID) IsNil() bool {
62
+
zero, err := ulid.ParseString("0000000000000000")
63
+
if err != nil {
64
+
panic(err)
65
+
}
66
+
67
+
return ulid.ULID(u) == zero
68
+
}
69
+
70
+
func (u ULID) IsEmpty() bool {
71
+
return u.IsNil() || u.Length() == 0
72
+
}
73
+
74
+
func (u *ULID) IsNilPtr() bool {
75
+
return u == nil
76
+
}
77
+
78
+
func (u *ULID) IsEmptyPtr() bool {
79
+
return u.IsNilPtr() || u.IsEmpty()
80
+
}
-15
package.json
-15
package.json
···
1
-
{
2
-
"name": "puregarlicspace",
3
-
"version": "1.0.0",
4
-
"description": "",
5
-
"main": "index.js",
6
-
"scripts": {
7
-
"test": "echo \"Error: no test specified\" && exit 1"
8
-
},
9
-
"keywords": [],
10
-
"author": "",
11
-
"license": "ISC",
12
-
"devDependencies": {
13
-
"tailwindcss": "^3.4.7"
14
-
}
15
-
}
-56
pages/auth.templ
-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
-
}
-32
pages/home.templ
-32
pages/home.templ
···
1
-
package pages
2
-
3
-
import "github.com/puregarlic/space/types"
4
-
import "fmt"
5
-
6
-
templ Home(profileUrl string, posts []*types.Post) {
7
-
<!DOCTYPE html>
8
-
<html>
9
-
<head>
10
-
<title>Micropub and IndieAuth Server Demo</title>
11
-
<link rel="authorization_endpoint" href="/authorization">
12
-
<link rel="token_endpoint" href="/token">
13
-
<link rel="micropub" href="/micropub">
14
-
15
-
<link rel="stylesheet" href="/static/styles.css" />
16
-
</head>
17
-
<body>
18
-
<h1>Micropub and IndieAuth Server Demo</h1>
19
-
20
-
<p>Sign in on a website that supports IndieAuth. Use <code>{ profileUrl }</code> as your domain.</p>
21
-
22
-
<h2>Posts</h2>
23
-
24
-
<p>You can create posts using a Micropub client.</p>
25
-
<ul>
26
-
for _, post := range posts {
27
-
<li><a href={ templ.URL("/posts/" + post.ID) }>{ post.ID }</a> - { fmt.Sprint(post.Properties["content"]) }</li>
28
-
}
29
-
</ul>
30
-
</body>
31
-
</html>
32
-
}
-56
pages/post.templ
-56
pages/post.templ
···
1
-
package pages
2
-
3
-
import "github.com/puregarlic/space/types"
4
-
import "fmt"
5
-
import "encoding/json"
6
-
import "reflect"
7
-
8
-
func printProperty(post *types.Post, name string) string {
9
-
if val, ok := post.Properties[name]; ok {
10
-
tp := reflect.TypeOf(val)
11
-
switch tp.Kind() {
12
-
default:
13
-
return fmt.Sprint(val)
14
-
case reflect.Slice:
15
-
str := ""
16
-
for _, v := range val {
17
-
str = str + fmt.Sprint(v)
18
-
}
19
-
20
-
return str
21
-
}
22
-
23
-
}
24
-
25
-
return "<no name provided>"
26
-
}
27
-
28
-
func printPost(post *types.Post) string {
29
-
out, err := json.Marshal(post)
30
-
31
-
if (err != nil) {
32
-
panic (err)
33
-
}
34
-
35
-
return fmt.Sprint(string(out))
36
-
}
37
-
38
-
templ Post(post *types.Post) {
39
-
<!DOCTYPE html>
40
-
<html>
41
-
<head>
42
-
<title>Post | Micropub and IndieAuth Server Demo</title>
43
-
</head>
44
-
<body>
45
-
<div class={ post.Type }>
46
-
<h1 class="p-name">{ printProperty(post, "name") }</h1>
47
-
<p class="p-content">{ printProperty(post, "content") }</p>
48
-
49
-
<h3>Stored Microformats</h3>
50
-
<code>
51
-
<pre>{ printPost(post) }</pre>
52
-
</code>
53
-
</div>
54
-
</body>
55
-
</html>
56
-
}
-842
pnpm-lock.yaml
-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
+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
+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
+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
+
}
+1
static/.gitkeep
+1
static/.gitkeep
···
1
+
+47
storage/cache.go
+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
+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
+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
+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
-11
tailwind.config.js