Compare commits
24 Commits
61228b4586
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d6ca4b815 | |||
| 4482529f6a | |||
| 0f53e8f659 | |||
| 4d746027dc | |||
| f7ab09c2c3 | |||
| fb347b96df | |||
| a8412af0c2 | |||
| 625280c327 | |||
| 01073aca78 | |||
| dd778174a7 | |||
| 49709ad3d2 | |||
| 3f49d528e7 | |||
| 46b67f8a6e | |||
| 5fc22c36b0 | |||
| 75442d261d | |||
| 0c3797ec30 | |||
| b78ec08cd1 | |||
| 2cb70e2fe7 | |||
| f6d2579b51 | |||
| 1390c2b97f | |||
| f13b3c80de | |||
| 39aaf00a6f | |||
| b60ab8b2de | |||
| b82a257df3 |
22
.drone.yml
22
.drone.yml
@@ -1,13 +1,17 @@
|
|||||||
kind: pipeline
|
kind: pipeline
|
||||||
name: test-docker-build
|
name: test-docker-build
|
||||||
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: test-docker-build
|
- name: test
|
||||||
image: plugins/docker
|
image: docker:dind
|
||||||
settings:
|
volumes:
|
||||||
repo: foo/bar
|
- name: dockersock
|
||||||
dry_run: true
|
path: /var/run/docker.sock
|
||||||
target: final
|
commands:
|
||||||
tags:
|
#- echo nameserver 116.202.204.30 > /etc/resolv.conf
|
||||||
- ${DRONE_COMMIT_SHA}
|
- docker build -t sneak/gohttpserver .
|
||||||
- ${DRONE_BRANCH}
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
|||||||
12
.gitea/workflows/check.yml
Normal file
12
.gitea/workflows/check.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
- run: docker build .
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
/httpd
|
/httpd
|
||||||
debug.log
|
debug.log
|
||||||
/.env
|
/.env
|
||||||
|
/cmd/httpd/httpd
|
||||||
|
|||||||
80
CLAUDE.md
Normal file
80
CLAUDE.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Repository Rules
|
||||||
|
|
||||||
|
Last Updated 2026-01-10
|
||||||
|
|
||||||
|
These rules MUST be followed at all times, it is very important.
|
||||||
|
|
||||||
|
* Do NOT stop working while there are still incomplete TODOs in the todo list
|
||||||
|
or in TODO.md. Continue implementing until all tasks are complete or you are
|
||||||
|
explicitly told to stop.
|
||||||
|
|
||||||
|
* Never use `git add -A` - add specific changes to a deliberate commit. A
|
||||||
|
commit should contain one change. After each change, make a commit with a
|
||||||
|
good one-line summary.
|
||||||
|
|
||||||
|
* NEVER modify the linter config without asking first.
|
||||||
|
|
||||||
|
* NEVER modify tests to exclude special cases or otherwise get them to pass
|
||||||
|
without asking first. In almost all cases, the code should be changed,
|
||||||
|
NOT the tests. If you think the test needs to be changed, make your case
|
||||||
|
for that and ask for permission to proceed, then stop. You need explicit
|
||||||
|
user approval to modify existing tests. (You do not need user approval
|
||||||
|
for writing NEW tests.)
|
||||||
|
|
||||||
|
* When linting, assume the linter config is CORRECT, and that each item
|
||||||
|
output by the linter is something that legitimately needs fixing in the
|
||||||
|
code.
|
||||||
|
|
||||||
|
* When running tests, use `make test`.
|
||||||
|
|
||||||
|
* Before commits, run `make check`. This runs `make lint` and `make test`
|
||||||
|
and `make check-fmt`. Any issues discovered MUST be resolved before
|
||||||
|
committing unless explicitly told otherwise.
|
||||||
|
|
||||||
|
* When fixing a bug, write a failing test for the bug FIRST. Add
|
||||||
|
appropriate logging to the test to ensure it is written correctly. Commit
|
||||||
|
that. Then go about fixing the bug until the test passes (without
|
||||||
|
modifying the test further). Then commit that.
|
||||||
|
|
||||||
|
* When adding a new feature, do the same - implement a test first (TDD). It
|
||||||
|
doesn't have to be super complex. Commit the test, then commit the
|
||||||
|
feature.
|
||||||
|
|
||||||
|
* When adding a new feature, use a feature branch. When the feature is
|
||||||
|
completely finished and the code is up to standards (passes `make check`)
|
||||||
|
then and only then can the feature branch be merged into `main` and the
|
||||||
|
branch deleted.
|
||||||
|
|
||||||
|
* Write godoc documentation comments for all exported types and functions as
|
||||||
|
you go along.
|
||||||
|
|
||||||
|
* ALWAYS be consistent in naming. If you name something one thing in one
|
||||||
|
place, name it the EXACT SAME THING in another place.
|
||||||
|
|
||||||
|
* Be descriptive and specific in naming. `wl` is bad;
|
||||||
|
`SourceHostWhitelist` is good. `ConnsPerHost` is bad;
|
||||||
|
`MaxConnectionsPerHost` is good.
|
||||||
|
|
||||||
|
* This is not prototype or teaching code - this is designed for production.
|
||||||
|
Any security issues (such as denial of service) or other web
|
||||||
|
vulnerabilities are P1 bugs and must be added to TODO.md at the top.
|
||||||
|
|
||||||
|
* As this is production code, no stubbing of implementations unless
|
||||||
|
specifically instructed. We need working implementations.
|
||||||
|
|
||||||
|
* NEVER silently fall back to a different setting when a user's parameter
|
||||||
|
explicitly specifies a value. If a user requests format=webp and WebP
|
||||||
|
encoding is not supported, return an error - do NOT silently output PNG
|
||||||
|
instead. If a user specifies fit=invalid and that fit mode doesn't exist,
|
||||||
|
return an error - do NOT silently default to "cover". Silent fallbacks
|
||||||
|
violate the principle of least surprise and mask bugs. The only acceptable
|
||||||
|
defaults are for OMITTED parameters, never for INVALID explicit values.
|
||||||
|
|
||||||
|
* Avoid vendoring deps unless specifically instructed to. NEVER commit
|
||||||
|
the vendor directory, NEVER commit compiled binaries. If these
|
||||||
|
directories or files exist, add them to .gitignore (and commit the
|
||||||
|
.gitignore) if they are not already in there. Keep the entire git
|
||||||
|
repository (with history) small - under 20MiB, unless you specifically
|
||||||
|
must commit larger files (e.g. test fixture example media files). Only
|
||||||
|
OUR source code and immediately supporting files (such as test examples)
|
||||||
|
goes into the repo/history.
|
||||||
1225
CONVENTIONS.md
Normal file
1225
CONVENTIONS.md
Normal file
File diff suppressed because it is too large
Load Diff
58
Dockerfile
58
Dockerfile
@@ -1,41 +1,35 @@
|
|||||||
## lint image
|
# Lint stage — fast feedback
|
||||||
FROM golangci/golangci-lint@sha256:9ae3767101cd3468cdaea5b6573dadb358013e05ac38abe37d53646680fd386c AS linter
|
# golangci/golangci-lint:v1.64.8 (2025-03-17)
|
||||||
|
FROM golangci/golangci-lint@sha256:2987913e27f4eca9c8a39129d2c7bc1e74fbcf77f181e01cea607be437aa5cb8 AS lint
|
||||||
RUN mkdir -p /build
|
WORKDIR /src
|
||||||
WORKDIR /build
|
COPY go.mod go.sum ./
|
||||||
COPY ./ ./
|
|
||||||
RUN golangci-lint run
|
|
||||||
|
|
||||||
## build image:
|
|
||||||
FROM golang:1.15-buster AS builder
|
|
||||||
|
|
||||||
RUN apt update && apt install -y make bzip2
|
|
||||||
|
|
||||||
RUN mkdir -p /build
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
COPY go.mod .
|
|
||||||
COPY go.sum .
|
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN make fmt-check
|
||||||
|
RUN make lint
|
||||||
|
|
||||||
COPY ./ ./
|
# Build stage
|
||||||
#RUN make lint
|
# golang:1.24-bookworm (Go 1.24)
|
||||||
RUN make rice-install && make httpd
|
FROM golang@sha256:1a6d4452c65dea36aac2e2d606b01b4a029ec90cc1ae53890540ce6173ea77ac AS builder
|
||||||
RUN go mod vendor
|
# Force BuildKit to run the lint stage
|
||||||
RUN tar -c . | bzip2 > /src.tbz2
|
COPY --from=lint /src/go.sum /dev/null
|
||||||
|
WORKDIR /build
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN make test
|
||||||
|
RUN make build && cp ./httpd /httpd
|
||||||
|
|
||||||
## output image:
|
# Runtime stage
|
||||||
FROM debian:buster-slim AS final
|
# debian:bookworm-slim (2025-03)
|
||||||
|
FROM debian@sha256:74d56e3931e0d5a1dd51f8c8a2466d21de84a271cd3b5a733b803aa91abf4421 AS final
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
COPY --from=builder /build/httpd /app/httpd
|
COPY --from=builder /httpd /app/httpd
|
||||||
COPY --from=builder /src.tbz2 /usr/local/src/src.tbz2
|
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV HOME /app
|
ENV HOME=/app
|
||||||
|
ENV PORT=8080
|
||||||
ENV PORT 8080
|
ENV DBURL=none
|
||||||
ENV DBURL none
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|||||||
29
Makefile
29
Makefile
@@ -4,7 +4,6 @@ VERSION := $(shell git describe --always --dirty=-dirty)
|
|||||||
ARCH := $(shell uname -m)
|
ARCH := $(shell uname -m)
|
||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
GOLDFLAGS += -X main.Version=$(VERSION)
|
GOLDFLAGS += -X main.Version=$(VERSION)
|
||||||
GOLDFLAGS += -X main.Buildarch=$(ARCH)
|
|
||||||
GOFLAGS := -ldflags "$(GOLDFLAGS)"
|
GOFLAGS := -ldflags "$(GOLDFLAGS)"
|
||||||
|
|
||||||
default: clean debug
|
default: clean debug
|
||||||
@@ -12,17 +11,30 @@ default: clean debug
|
|||||||
commit: fmt lint
|
commit: fmt lint
|
||||||
git commit -a
|
git commit -a
|
||||||
|
|
||||||
# get golangci-lint with:
|
# get gofumpt with:
|
||||||
# go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.31.0
|
# go install mvdan.cc/gofumpt@latest
|
||||||
# get gofumports with:
|
|
||||||
# go get mvdan.cc/gofumpt/gofumports
|
|
||||||
fmt:
|
fmt:
|
||||||
gofumpt -l -w .
|
gofumpt -l -w .
|
||||||
golangci-lint run --fix
|
golangci-lint run --fix
|
||||||
|
|
||||||
|
fmt-check:
|
||||||
|
@test -z "$$(gofmt -l .)" || { echo "gofmt found unformatted files:"; gofmt -l .; exit 1; }
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run
|
golangci-lint run
|
||||||
sh -c 'test -z "$$(gofmt -l .)"'
|
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
check: fmt-check lint test
|
||||||
|
|
||||||
|
build: ./$(FN)d
|
||||||
|
|
||||||
|
hooks:
|
||||||
|
@mkdir -p .git/hooks
|
||||||
|
@printf '#!/bin/sh\nmake fmt-check lint\n' > .git/hooks/pre-commit
|
||||||
|
@chmod +x .git/hooks/pre-commit
|
||||||
|
@echo "Pre-commit hook installed."
|
||||||
|
|
||||||
debug: ./$(FN)d
|
debug: ./$(FN)d
|
||||||
DEBUG=1 GOTRACEBACK=all ./$(FN)d
|
DEBUG=1 GOTRACEBACK=all ./$(FN)d
|
||||||
@@ -44,5 +56,6 @@ docker:
|
|||||||
go build -o ../../$(FN)d $(GOFLAGS) .
|
go build -o ../../$(FN)d $(GOFLAGS) .
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
go get -v github.com/golangci/golangci-lint/cmd/golangci-lint@v1.31.0
|
go install mvdan.cc/gofumpt@latest
|
||||||
go get -v mvdan.cc/gofumpt/gofumports
|
|
||||||
|
.PHONY: default commit fmt fmt-check lint test check build hooks debug debugger run clean docker tools
|
||||||
|
|||||||
@@ -86,3 +86,4 @@ WTFPL (aka public domain):
|
|||||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
71
TODO.md
Normal file
71
TODO.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# TODO: Blog Posts CRUD Implementation
|
||||||
|
|
||||||
|
## Completed
|
||||||
|
|
||||||
|
- [x] Add SQLite dependency (`modernc.org/sqlite` - pure Go, no CGO)
|
||||||
|
- [x] Create models package (`internal/models/models.go`)
|
||||||
|
- Post struct with JSON tags
|
||||||
|
- CreatePostRequest and UpdatePostRequest structs
|
||||||
|
- [x] Update database package (`internal/database/database.go`)
|
||||||
|
- SQLite connection with WAL mode
|
||||||
|
- Schema auto-creation on startup
|
||||||
|
- CRUD methods: CreatePost, GetPost, ListPosts, UpdatePost, DeletePost
|
||||||
|
- [x] Create post handlers (`internal/handlers/posts.go`)
|
||||||
|
- HandleListPosts, HandleGetPost, HandleCreatePost, HandleUpdatePost, HandleDeletePost
|
||||||
|
- [x] Register API routes (`internal/server/routes.go`)
|
||||||
|
- GET /api/v1/posts
|
||||||
|
- POST /api/v1/posts
|
||||||
|
- GET /api/v1/posts/{id}
|
||||||
|
- PUT /api/v1/posts/{id}
|
||||||
|
- DELETE /api/v1/posts/{id}
|
||||||
|
- [x] Set default DBURL (`internal/config/config.go`)
|
||||||
|
- Default: `file:./data.db?_journal_mode=WAL`
|
||||||
|
|
||||||
|
## Optional (Future Enhancements)
|
||||||
|
|
||||||
|
- [ ] Add HTML templates for web UI
|
||||||
|
- `templates/posts.html` - List all posts
|
||||||
|
- `templates/post.html` - View single post
|
||||||
|
- `templates/post_form.html` - Create/edit form
|
||||||
|
- [ ] Add web routes for HTML views
|
||||||
|
- GET /posts - List page
|
||||||
|
- GET /posts/new - Create form
|
||||||
|
- GET /posts/{id} - View page
|
||||||
|
- GET /posts/{id}/edit - Edit form
|
||||||
|
- [ ] Add authentication for create/update/delete operations
|
||||||
|
- [ ] Add pagination for list endpoint
|
||||||
|
- [ ] Add unit tests
|
||||||
|
|
||||||
|
## Testing the API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a post
|
||||||
|
curl -X POST http://localhost:8080/api/v1/posts \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"title":"Hello","body":"World","author":"me"}'
|
||||||
|
|
||||||
|
# List all posts
|
||||||
|
curl http://localhost:8080/api/v1/posts
|
||||||
|
|
||||||
|
# Get a single post
|
||||||
|
curl http://localhost:8080/api/v1/posts/1
|
||||||
|
|
||||||
|
# Update a post
|
||||||
|
curl -X PUT http://localhost:8080/api/v1/posts/1 \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"title":"Updated Title","published":true}'
|
||||||
|
|
||||||
|
# Delete a post
|
||||||
|
curl -X DELETE http://localhost:8080/api/v1/posts/1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Modified/Created
|
||||||
|
|
||||||
|
| File | Status |
|
||||||
|
|------|--------|
|
||||||
|
| `go.mod` | Modified - added modernc.org/sqlite |
|
||||||
|
| `internal/models/models.go` | Created |
|
||||||
|
| `internal/database/database.go` | Modified - added SQLite and CRUD |
|
||||||
|
| `internal/handlers/posts.go` | Created |
|
||||||
|
| `internal/server/routes.go` | Modified - added post routes |
|
||||||
|
| `internal/config/config.go` | Modified - added default DBURL |
|
||||||
@@ -1,17 +1,37 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"git.eeqj.de/sneak/gohttpserver/internal/config"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/database"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/globals"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/handlers"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/healthcheck"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/logger"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/middleware"
|
||||||
"git.eeqj.de/sneak/gohttpserver/internal/server"
|
"git.eeqj.de/sneak/gohttpserver/internal/server"
|
||||||
|
"go.uber.org/fx"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Appname string = "CHANGEME"
|
Appname string = "CHANGEME"
|
||||||
Version string
|
Version string
|
||||||
Buildarch string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
os.Exit(server.Run(Appname, Version, Buildarch))
|
globals.Appname = Appname
|
||||||
|
globals.Version = Version
|
||||||
|
|
||||||
|
fx.New(
|
||||||
|
fx.Provide(
|
||||||
|
config.New,
|
||||||
|
database.New,
|
||||||
|
globals.New,
|
||||||
|
handlers.New,
|
||||||
|
logger.New,
|
||||||
|
server.New,
|
||||||
|
middleware.New,
|
||||||
|
healthcheck.New,
|
||||||
|
),
|
||||||
|
fx.Invoke(func(*server.Server) {}),
|
||||||
|
).Run()
|
||||||
}
|
}
|
||||||
|
|||||||
22
go.mod
22
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module git.eeqj.de/sneak/gohttpserver
|
module git.eeqj.de/sneak/gohttpserver
|
||||||
|
|
||||||
go 1.19
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d
|
github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d
|
||||||
@@ -9,36 +9,48 @@ require (
|
|||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
github.com/joho/godotenv v1.4.0
|
github.com/joho/godotenv v1.4.0
|
||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/prometheus/client_golang v1.14.0
|
||||||
github.com/rs/zerolog v1.28.0
|
|
||||||
github.com/slok/go-http-metrics v0.10.0
|
github.com/slok/go-http-metrics v0.10.0
|
||||||
github.com/spf13/viper v1.14.0
|
github.com/spf13/viper v1.14.0
|
||||||
|
go.uber.org/fx v1.18.2
|
||||||
|
modernc.org/sqlite v1.41.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.6 // indirect
|
github.com/magiconair/properties v1.8.6 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/spf13/afero v1.9.3 // indirect
|
github.com/spf13/afero v1.9.3 // indirect
|
||||||
github.com/spf13/cast v1.5.0 // indirect
|
github.com/spf13/cast v1.5.0 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.4.1 // indirect
|
github.com/subosito/gotenv v1.4.1 // indirect
|
||||||
golang.org/x/sys v0.2.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/dig v1.15.0 // indirect
|
||||||
|
go.uber.org/multierr v1.8.0 // indirect
|
||||||
|
go.uber.org/zap v1.21.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.4.0 // indirect
|
golang.org/x/text v0.4.0 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.66.10 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
101
go.sum
101
go.sum
@@ -45,6 +45,9 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
|||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
|
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||||
|
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -60,10 +63,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
@@ -71,6 +75,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||||
|
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
|
github.com/getsentry/sentry-go v0.15.0 h1:CP9bmA7pralrVUedYZsmIHWpq/pBtXTSew7xvVpfLaA=
|
||||||
@@ -80,6 +85,7 @@ github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
|
|||||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
|
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
@@ -92,7 +98,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
|||||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@@ -134,7 +139,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
@@ -149,8 +155,12 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
|||||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||||
@@ -178,17 +188,15 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
|||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
|
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
|
||||||
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
|
||||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
@@ -201,11 +209,14 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
|||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||||
|
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -239,11 +250,11 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
|||||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
|
||||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
@@ -279,12 +290,27 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/dig v1.15.0 h1:vq3YWr8zRj1eFGC7Gvf907hE0eRjPTZ1d3xHadD6liE=
|
||||||
|
go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM=
|
||||||
|
go.uber.org/fx v1.18.2 h1:bUNI6oShr+OVFQeU8cDNbnN7VFsu+SsjHzUF51V/GAU=
|
||||||
|
go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY=
|
||||||
|
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||||
|
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||||
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
|
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
||||||
|
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||||
|
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
|
||||||
|
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
@@ -303,6 +329,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -326,6 +354,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -359,10 +390,12 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
|
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
|
||||||
|
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -384,6 +417,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -423,18 +459,18 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -497,6 +533,9 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
|
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -605,10 +644,12 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
@@ -618,6 +659,32 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||||
|
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||||
|
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||||
|
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
|
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||||
|
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.41.0 h1:bJXddp4ZpsqMsNN1vS0jWo4IJTZzb8nWpcgvyCFG9Ck=
|
||||||
|
modernc.org/sqlite v1.41.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|||||||
98
internal/config/config.go
Normal file
98
internal/config/config.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/globals"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/logger"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
|
||||||
|
// spooky action at a distance!
|
||||||
|
// this populates the environment
|
||||||
|
// from a ./.env file automatically
|
||||||
|
// for development configuration.
|
||||||
|
// .env contents should be things like
|
||||||
|
// `DBURL=postgres://user:pass@.../`
|
||||||
|
// (without the backticks, of course)
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigParams struct {
|
||||||
|
fx.In
|
||||||
|
Globals *globals.Globals
|
||||||
|
Logger *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
DBURL string
|
||||||
|
Debug bool
|
||||||
|
MaintenanceMode bool
|
||||||
|
DevelopmentMode bool
|
||||||
|
DevAdminUsername string
|
||||||
|
DevAdminPassword string
|
||||||
|
MetricsPassword string
|
||||||
|
MetricsUsername string
|
||||||
|
Port int
|
||||||
|
SentryDSN string
|
||||||
|
params *ConfigParams
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
||||||
|
log := params.Logger.Get()
|
||||||
|
name := params.Globals.Appname
|
||||||
|
|
||||||
|
viper.SetConfigName(name)
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
// path to look for the config file in:
|
||||||
|
viper.AddConfigPath(fmt.Sprintf("/etc/%s", name))
|
||||||
|
// call multiple times to add many search paths:
|
||||||
|
viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", name))
|
||||||
|
// viper.SetEnvPrefix(strings.ToUpper(s.appname))
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
viper.SetDefault("DEBUG", "false")
|
||||||
|
viper.SetDefault("MAINTENANCE_MODE", "false")
|
||||||
|
viper.SetDefault("DEVELOPMENT_MODE", "false")
|
||||||
|
viper.SetDefault("DEV_ADMIN_USERNAME", "")
|
||||||
|
viper.SetDefault("DEV_ADMIN_PASSWORD", "")
|
||||||
|
viper.SetDefault("PORT", "8080")
|
||||||
|
viper.SetDefault("DBURL", "file:./data.db?_journal_mode=WAL")
|
||||||
|
viper.SetDefault("SENTRY_DSN", "")
|
||||||
|
viper.SetDefault("METRICS_USERNAME", "")
|
||||||
|
viper.SetDefault("METRICS_PASSWORD", "")
|
||||||
|
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||||
|
// Config file not found; ignore error if desired
|
||||||
|
} else {
|
||||||
|
// Config file was found but another error was produced
|
||||||
|
log.Error("config file malformed", "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Config{
|
||||||
|
DBURL: viper.GetString("DBURL"),
|
||||||
|
Debug: viper.GetBool("debug"),
|
||||||
|
Port: viper.GetInt("PORT"),
|
||||||
|
SentryDSN: viper.GetString("SENTRY_DSN"),
|
||||||
|
MaintenanceMode: viper.GetBool("MAINTENANCE_MODE"),
|
||||||
|
DevelopmentMode: viper.GetBool("DEVELOPMENT_MODE"),
|
||||||
|
DevAdminUsername: viper.GetString("DEV_ADMIN_USERNAME"),
|
||||||
|
DevAdminPassword: viper.GetString("DEV_ADMIN_PASSWORD"),
|
||||||
|
MetricsUsername: viper.GetString("METRICS_USERNAME"),
|
||||||
|
MetricsPassword: viper.GetString("METRICS_PASSWORD"),
|
||||||
|
log: log,
|
||||||
|
params: ¶ms,
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Debug {
|
||||||
|
params.Logger.EnableDebugLogging()
|
||||||
|
s.log = params.Logger.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
206
internal/database/database.go
Normal file
206
internal/database/database.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/config"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/logger"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/models"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
|
||||||
|
// spooky action at a distance!
|
||||||
|
// this populates the environment
|
||||||
|
// from a ./.env file automatically
|
||||||
|
// for development configuration.
|
||||||
|
// .env contents should be things like
|
||||||
|
// `DBURL=postgres://user:pass@.../`
|
||||||
|
// (without the backticks, of course)
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
|
||||||
|
// pure Go SQLite driver
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DatabaseParams struct {
|
||||||
|
fx.In
|
||||||
|
Logger *logger.Logger
|
||||||
|
Config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
db *sql.DB
|
||||||
|
log *slog.Logger
|
||||||
|
params *DatabaseParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) {
|
||||||
|
s := new(Database)
|
||||||
|
s.params = ¶ms
|
||||||
|
s.log = params.Logger.Get()
|
||||||
|
|
||||||
|
s.log.Info("Database instantiated")
|
||||||
|
|
||||||
|
lc.Append(fx.Hook{
|
||||||
|
OnStart: func(ctx context.Context) error {
|
||||||
|
s.log.Info("Database OnStart Hook")
|
||||||
|
return s.connect(ctx)
|
||||||
|
},
|
||||||
|
OnStop: func(ctx context.Context) error {
|
||||||
|
s.log.Info("Database OnStop Hook")
|
||||||
|
if s.db != nil {
|
||||||
|
return s.db.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Database) connect(ctx context.Context) error {
|
||||||
|
dbURL := s.params.Config.DBURL
|
||||||
|
if dbURL == "" {
|
||||||
|
dbURL = "file:./data.db?_journal_mode=WAL"
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.Info("connecting to database", "url", dbURL)
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite", dbURL)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to open database", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
s.log.Error("failed to ping database", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.db = db
|
||||||
|
s.log.Info("database connected")
|
||||||
|
|
||||||
|
return s.createSchema(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Database) createSchema(ctx context.Context) error {
|
||||||
|
schema := `
|
||||||
|
CREATE TABLE IF NOT EXISTS posts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
author TEXT NOT NULL,
|
||||||
|
published INTEGER DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)`
|
||||||
|
|
||||||
|
_, err := s.db.ExecContext(ctx, schema)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to create schema", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.Info("database schema initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Database) CreatePost(ctx context.Context, req *models.CreatePostRequest) (*models.Post, error) {
|
||||||
|
now := time.Now()
|
||||||
|
result, err := s.db.ExecContext(ctx,
|
||||||
|
`INSERT INTO posts (title, body, author, published, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
|
req.Title, req.Body, req.Author, req.Published, now, now,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetPost(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Database) GetPost(ctx context.Context, id int64) (*models.Post, error) {
|
||||||
|
post := &models.Post{}
|
||||||
|
err := s.db.QueryRowContext(ctx,
|
||||||
|
`SELECT id, title, body, author, published, created_at, updated_at FROM posts WHERE id = ?`,
|
||||||
|
id,
|
||||||
|
).Scan(&post.ID, &post.Title, &post.Body, &post.Author, &post.Published, &post.CreatedAt, &post.UpdatedAt)
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Database) ListPosts(ctx context.Context) ([]*models.Post, error) {
|
||||||
|
rows, err := s.db.QueryContext(ctx,
|
||||||
|
`SELECT id, title, body, author, published, created_at, updated_at FROM posts ORDER BY created_at DESC`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var posts []*models.Post
|
||||||
|
for rows.Next() {
|
||||||
|
post := &models.Post{}
|
||||||
|
if err := rows.Scan(&post.ID, &post.Title, &post.Body, &post.Author, &post.Published, &post.CreatedAt, &post.UpdatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
posts = append(posts, post)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Database) UpdatePost(ctx context.Context, id int64, req *models.UpdatePostRequest) (*models.Post, error) {
|
||||||
|
post, err := s.GetPost(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if post == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Title != nil {
|
||||||
|
post.Title = *req.Title
|
||||||
|
}
|
||||||
|
if req.Body != nil {
|
||||||
|
post.Body = *req.Body
|
||||||
|
}
|
||||||
|
if req.Author != nil {
|
||||||
|
post.Author = *req.Author
|
||||||
|
}
|
||||||
|
if req.Published != nil {
|
||||||
|
post.Published = *req.Published
|
||||||
|
}
|
||||||
|
post.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
_, err = s.db.ExecContext(ctx,
|
||||||
|
`UPDATE posts SET title = ?, body = ?, author = ?, published = ?, updated_at = ? WHERE id = ?`,
|
||||||
|
post.Title, post.Body, post.Author, post.Published, post.UpdatedAt, id,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Database) DeletePost(ctx context.Context, id int64) error {
|
||||||
|
_, err := s.db.ExecContext(ctx, `DELETE FROM posts WHERE id = ?`, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
24
internal/globals/globals.go
Normal file
24
internal/globals/globals.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package globals
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// these get populated from main() and copied into the Globals object.
|
||||||
|
var (
|
||||||
|
Appname string
|
||||||
|
Version string
|
||||||
|
)
|
||||||
|
|
||||||
|
type Globals struct {
|
||||||
|
Appname string
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(lc fx.Lifecycle) (*Globals, error) {
|
||||||
|
n := &Globals{
|
||||||
|
Appname: Appname,
|
||||||
|
Version: Version,
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
57
internal/handlers/handlers.go
Normal file
57
internal/handlers/handlers.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/database"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/globals"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/healthcheck"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/logger"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandlersParams struct {
|
||||||
|
fx.In
|
||||||
|
Logger *logger.Logger
|
||||||
|
Globals *globals.Globals
|
||||||
|
Database *database.Database
|
||||||
|
Healthcheck *healthcheck.Healthcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handlers struct {
|
||||||
|
params *HandlersParams
|
||||||
|
log *slog.Logger
|
||||||
|
hc *healthcheck.Healthcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
|
||||||
|
s := new(Handlers)
|
||||||
|
s.params = ¶ms
|
||||||
|
s.log = params.Logger.Get()
|
||||||
|
s.hc = params.Healthcheck
|
||||||
|
lc.Append(fx.Hook{
|
||||||
|
OnStart: func(ctx context.Context) error {
|
||||||
|
// FIXME compile some templates here or something
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handlers) respondJSON(w http.ResponseWriter, r *http.Request, data interface{}, status int) {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if data != nil {
|
||||||
|
err := json.NewEncoder(w).Encode(data)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("json encode error", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handlers) decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) error { // nolint
|
||||||
|
return json.NewDecoder(r.Body).Decode(v)
|
||||||
|
}
|
||||||
12
internal/handlers/healthcheck.go
Normal file
12
internal/handlers/healthcheck.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Handlers) HandleHealthCheck() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
resp := s.hc.Healthcheck()
|
||||||
|
s.respondJSON(w, req, resp, 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
internal/handlers/index.go
Normal file
19
internal/handlers/index.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Handlers) HandleIndex() http.HandlerFunc {
|
||||||
|
t := templates.GetParsed()
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := t.ExecuteTemplate(w, "index.html", nil)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("template execution failed", "error", err)
|
||||||
|
http.Error(w, http.StatusText(500), 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
internal/handlers/login.go
Normal file
19
internal/handlers/login.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Handlers) HandleLoginGET() http.HandlerFunc {
|
||||||
|
t := templates.GetParsed()
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := t.ExecuteTemplate(w, "login.html", nil)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("template execution failed", "error", err)
|
||||||
|
http.Error(w, http.StatusText(500), 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package server
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *server) handleNow() http.HandlerFunc {
|
func (s *Handlers) HandleNow() http.HandlerFunc {
|
||||||
type response struct {
|
type response struct {
|
||||||
Now time.Time `json:"now"`
|
Now time.Time `json:"now"`
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package server
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
// CHANGEME you probably want to remove this,
|
// CHANGEME you probably want to remove this,
|
||||||
// this is just a handler/route that throws a panic to test
|
// this is just a handler/route that throws a panic to test
|
||||||
// sentry events.
|
// sentry events.
|
||||||
func (s *server) handlePanic() http.HandlerFunc {
|
func (s *Handlers) HandlePanic() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
panic("y tho")
|
panic("y tho")
|
||||||
}
|
}
|
||||||
137
internal/handlers/posts.go
Normal file
137
internal/handlers/posts.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/models"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Handlers) HandleListPosts() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
posts, err := s.params.Database.ListPosts(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to list posts", "error", err)
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "internal server error"}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if posts == nil {
|
||||||
|
posts = []*models.Post{}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.respondJSON(w, r, posts, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handlers) HandleGetPost() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
idStr := chi.URLParam(r, "id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "invalid id"}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := s.params.Database.GetPost(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to get post", "error", err, "id", id)
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "internal server error"}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if post == nil {
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "not found"}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.respondJSON(w, r, post, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handlers) HandleCreatePost() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req models.CreatePostRequest
|
||||||
|
if err := s.decodeJSON(w, r, &req); err != nil {
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "invalid request body"}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Title == "" || req.Body == "" || req.Author == "" {
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "title, body, and author are required"}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := s.params.Database.CreatePost(r.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to create post", "error", err)
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "internal server error"}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.respondJSON(w, r, post, http.StatusCreated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handlers) HandleUpdatePost() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
idStr := chi.URLParam(r, "id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "invalid id"}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req models.UpdatePostRequest
|
||||||
|
if err := s.decodeJSON(w, r, &req); err != nil {
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "invalid request body"}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := s.params.Database.UpdatePost(r.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to update post", "error", err, "id", id)
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "internal server error"}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if post == nil {
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "not found"}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.respondJSON(w, r, post, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handlers) HandleDeletePost() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
idStr := chi.URLParam(r, "id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "invalid id"}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := s.params.Database.GetPost(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to get post for delete", "error", err, "id", id)
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "internal server error"}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if post == nil {
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "not found"}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.params.Database.DeletePost(r.Context(), id); err != nil {
|
||||||
|
s.log.Error("failed to delete post", "error", err, "id", id)
|
||||||
|
s.respondJSON(w, r, map[string]string{"error": "internal server error"}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.respondJSON(w, r, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
34
internal/handlers/signup.go
Normal file
34
internal/handlers/signup.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Handlers) HandleSignupGET() http.HandlerFunc {
|
||||||
|
t := templates.GetParsed()
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := t.ExecuteTemplate(w, "signup.html", nil)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("template execution failed", "error", err)
|
||||||
|
http.Error(w, http.StatusText(500), 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handlers) HandleSignupPOST() http.HandlerFunc {
|
||||||
|
t := templates.GetParsed()
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
_ = r.ParseForm()
|
||||||
|
|
||||||
|
err := t.ExecuteTemplate(w, "signup.html", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("template execution failed", "error", err)
|
||||||
|
http.Error(w, http.StatusText(500), 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
internal/healthcheck/healthcheck.go
Normal file
71
internal/healthcheck/healthcheck.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package healthcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/config"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/database"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/globals"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/logger"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HealthcheckParams struct {
|
||||||
|
fx.In
|
||||||
|
Globals *globals.Globals
|
||||||
|
Config *config.Config
|
||||||
|
Logger *logger.Logger
|
||||||
|
Database *database.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
type Healthcheck struct {
|
||||||
|
StartupTime time.Time
|
||||||
|
log *slog.Logger
|
||||||
|
params *HealthcheckParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(lc fx.Lifecycle, params HealthcheckParams) (*Healthcheck, error) {
|
||||||
|
s := new(Healthcheck)
|
||||||
|
s.params = ¶ms
|
||||||
|
s.log = params.Logger.Get()
|
||||||
|
|
||||||
|
lc.Append(fx.Hook{
|
||||||
|
OnStart: func(ctx context.Context) error {
|
||||||
|
s.StartupTime = time.Now()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnStop: func(ctx context.Context) error {
|
||||||
|
// FIXME do server shutdown here
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealthcheckResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Now string `json:"now"`
|
||||||
|
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||||
|
UptimeHuman string `json:"uptime_human"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Appname string `json:"appname"`
|
||||||
|
Maintenance bool `json:"maintenance_mode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Healthcheck) uptime() time.Duration {
|
||||||
|
return time.Since(s.StartupTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Healthcheck) Healthcheck() *HealthcheckResponse {
|
||||||
|
resp := &HealthcheckResponse{
|
||||||
|
Status: "ok",
|
||||||
|
Now: time.Now().UTC().Format(time.RFC3339Nano),
|
||||||
|
UptimeSeconds: int64(s.uptime().Seconds()),
|
||||||
|
UptimeHuman: s.uptime().String(),
|
||||||
|
Appname: s.params.Globals.Appname,
|
||||||
|
Version: s.params.Globals.Version,
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
68
internal/logger/logger.go
Normal file
68
internal/logger/logger.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/globals"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoggerParams struct {
|
||||||
|
fx.In
|
||||||
|
Globals *globals.Globals
|
||||||
|
}
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
log *slog.Logger
|
||||||
|
level *slog.LevelVar
|
||||||
|
params LoggerParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(lc fx.Lifecycle, params LoggerParams) (*Logger, error) {
|
||||||
|
l := new(Logger)
|
||||||
|
l.level = new(slog.LevelVar)
|
||||||
|
l.level.Set(slog.LevelInfo)
|
||||||
|
|
||||||
|
// TTY detection for dev vs prod output
|
||||||
|
tty := false
|
||||||
|
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
||||||
|
tty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler slog.Handler
|
||||||
|
if tty {
|
||||||
|
// Text output for development
|
||||||
|
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: l.level,
|
||||||
|
AddSource: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// JSON output for production
|
||||||
|
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: l.level,
|
||||||
|
AddSource: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
l.log = slog.New(handler)
|
||||||
|
l.params = params
|
||||||
|
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) EnableDebugLogging() {
|
||||||
|
l.level.Set(slog.LevelDebug)
|
||||||
|
l.log.Debug("debug logging enabled", "debug", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Get() *slog.Logger {
|
||||||
|
return l.log
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Identify() {
|
||||||
|
l.log.Info("starting",
|
||||||
|
"appname", l.params.Globals.Appname,
|
||||||
|
"version", l.params.Globals.Version,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package server
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/config"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/globals"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/logger"
|
||||||
basicauth "github.com/99designs/basicauth-go"
|
basicauth "github.com/99designs/basicauth-go"
|
||||||
"github.com/go-chi/chi/middleware"
|
"github.com/go-chi/chi/middleware"
|
||||||
"github.com/go-chi/cors"
|
"github.com/go-chi/cors"
|
||||||
@@ -12,10 +16,27 @@ import (
|
|||||||
ghmm "github.com/slok/go-http-metrics/middleware"
|
ghmm "github.com/slok/go-http-metrics/middleware"
|
||||||
"github.com/slok/go-http-metrics/middleware/std"
|
"github.com/slok/go-http-metrics/middleware/std"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"go.uber.org/fx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// the following is from
|
type MiddlewareParams struct {
|
||||||
// https://learning-cloud-native-go.github.io/docs/a6.adding_zerolog_logger/
|
fx.In
|
||||||
|
Logger *logger.Logger
|
||||||
|
Globals *globals.Globals
|
||||||
|
Config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Middleware struct {
|
||||||
|
log *slog.Logger
|
||||||
|
params *MiddlewareParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(lc fx.Lifecycle, params MiddlewareParams) (*Middleware, error) {
|
||||||
|
s := new(Middleware)
|
||||||
|
s.params = ¶ms
|
||||||
|
s.log = params.Logger.Get()
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ipFromHostPort(hp string) string {
|
func ipFromHostPort(hp string) string {
|
||||||
h, _, err := net.SplitHostPort(hp)
|
h, _, err := net.SplitHostPort(hp)
|
||||||
@@ -45,8 +66,7 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
|||||||
// type Middleware func(http.Handler) http.Handler
|
// type Middleware func(http.Handler) http.Handler
|
||||||
// this returns a Middleware that is designed to do every request through the
|
// this returns a Middleware that is designed to do every request through the
|
||||||
// mux, note the signature:
|
// mux, note the signature:
|
||||||
func (s *server) LoggingMiddleware() func(http.Handler) http.Handler {
|
func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
||||||
// FIXME this should use https://github.com/google/go-cloud/blob/master/server/requestlog/requestlog.go
|
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
@@ -54,18 +74,18 @@ func (s *server) LoggingMiddleware() func(http.Handler) http.Handler {
|
|||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
defer func() {
|
defer func() {
|
||||||
latency := time.Since(start)
|
latency := time.Since(start)
|
||||||
s.log.Info().
|
s.log.InfoContext(ctx, "request",
|
||||||
Time("request_start", start).
|
"request_start", start,
|
||||||
Str("method", r.Method).
|
"method", r.Method,
|
||||||
Str("url", r.URL.String()).
|
"url", r.URL.String(),
|
||||||
Str("useragent", r.UserAgent()).
|
"useragent", r.UserAgent(),
|
||||||
Str("request_id", ctx.Value(middleware.RequestIDKey).(string)).
|
"request_id", ctx.Value(middleware.RequestIDKey).(string),
|
||||||
Str("referer", r.Referer()).
|
"referer", r.Referer(),
|
||||||
Str("proto", r.Proto).
|
"proto", r.Proto,
|
||||||
Str("remoteIP", ipFromHostPort(r.RemoteAddr)).
|
"remoteIP", ipFromHostPort(r.RemoteAddr),
|
||||||
Int("status", lrw.statusCode).
|
"status", lrw.statusCode,
|
||||||
Int("latency_ms", int(latency.Milliseconds())).
|
"latency_ms", latency.Milliseconds(),
|
||||||
Send()
|
)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
next.ServeHTTP(lrw, r)
|
next.ServeHTTP(lrw, r)
|
||||||
@@ -73,7 +93,7 @@ func (s *server) LoggingMiddleware() func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) CORSMiddleware() func(http.Handler) http.Handler {
|
func (s *Middleware) CORS() func(http.Handler) http.Handler {
|
||||||
return cors.Handler(cors.Options{
|
return cors.Handler(cors.Options{
|
||||||
// CHANGEME! these are defaults, change them to suit your needs or
|
// CHANGEME! these are defaults, change them to suit your needs or
|
||||||
// read from environment/viper.
|
// read from environment/viper.
|
||||||
@@ -88,17 +108,17 @@ func (s *server) CORSMiddleware() func(http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) AuthMiddleware() func(http.Handler) http.Handler {
|
func (s *Middleware) Auth() func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// CHANGEME you'll want to change this to do stuff.
|
// CHANGEME you'll want to change this to do stuff.
|
||||||
s.log.Info().Msg("AUTH: before request")
|
s.log.Info("AUTH: before request")
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) MetricsMiddleware() func(http.Handler) http.Handler {
|
func (s *Middleware) Metrics() func(http.Handler) http.Handler {
|
||||||
mdlw := ghmm.New(ghmm.Config{
|
mdlw := ghmm.New(ghmm.Config{
|
||||||
Recorder: metrics.NewRecorder(metrics.Config{}),
|
Recorder: metrics.NewRecorder(metrics.Config{}),
|
||||||
})
|
})
|
||||||
@@ -107,7 +127,7 @@ func (s *server) MetricsMiddleware() func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) MetricsAuthMiddleware() func(http.Handler) http.Handler {
|
func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler {
|
||||||
return basicauth.New(
|
return basicauth.New(
|
||||||
"metrics",
|
"metrics",
|
||||||
map[string][]string{
|
map[string][]string{
|
||||||
29
internal/models/models.go
Normal file
29
internal/models/models.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Published bool `json:"published"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePostRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Published bool `json:"published"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePostRequest struct {
|
||||||
|
Title *string `json:"title,omitempty"`
|
||||||
|
Body *string `json:"body,omitempty"`
|
||||||
|
Author *string `json:"author,omitempty"`
|
||||||
|
Published *bool `json:"published,omitempty"`
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *server) handleHealthCheck() http.HandlerFunc {
|
|
||||||
type response struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Now string `json:"now"`
|
|
||||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
|
||||||
UptimeHuman string `json:"uptime_human"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Appname string `json:"appname"`
|
|
||||||
Maintenance bool `json:"maintenance_mode"`
|
|
||||||
}
|
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
resp := &response{
|
|
||||||
Status: "ok",
|
|
||||||
Now: time.Now().UTC().Format(time.RFC3339Nano),
|
|
||||||
UptimeSeconds: int64(s.uptime().Seconds()),
|
|
||||||
UptimeHuman: s.uptime().String(),
|
|
||||||
Maintenance: s.maintenance(),
|
|
||||||
Appname: s.appname,
|
|
||||||
Version: s.version,
|
|
||||||
}
|
|
||||||
s.respondJSON(w, req, resp, 200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.eeqj.de/sneak/gohttpserver/templates"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *server) handleIndex() http.HandlerFunc {
|
|
||||||
indexTemplate := template.Must(template.New("index").Parse(templates.MustString("index.html")))
|
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := indexTemplate.ExecuteTemplate(w, "index", nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err.Error())
|
|
||||||
http.Error(w, http.StatusText(500), 500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *server) handleLogin() http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprintf(w, "hello login")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *server) serveUntilShutdown() {
|
func (s *Server) serveUntilShutdown() {
|
||||||
listenAddr := fmt.Sprintf(":%d", s.port)
|
listenAddr := fmt.Sprintf(":%d", s.params.Config.Port)
|
||||||
s.httpServer = &http.Server{
|
s.httpServer = &http.Server{
|
||||||
Addr: listenAddr,
|
Addr: listenAddr,
|
||||||
ReadTimeout: 10 * time.Second,
|
ReadTimeout: 10 * time.Second,
|
||||||
@@ -19,32 +18,17 @@ func (s *server) serveUntilShutdown() {
|
|||||||
|
|
||||||
// add routes
|
// add routes
|
||||||
// this does any necessary setup in each handler
|
// this does any necessary setup in each handler
|
||||||
s.routes()
|
s.SetupRoutes()
|
||||||
|
|
||||||
s.log.Info().Str("listenaddr", listenAddr).Msg("http begin listen")
|
s.log.Info("http begin listen", "listenaddr", listenAddr)
|
||||||
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
s.log.Error().Msgf("listen:%+s\n", err)
|
s.log.Error("listen error", "error", err)
|
||||||
if s.cancelFunc != nil {
|
if s.cancelFunc != nil {
|
||||||
s.cancelFunc()
|
s.cancelFunc()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) respondJSON(w http.ResponseWriter, r *http.Request, data interface{}, status int) {
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(status)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if data != nil {
|
|
||||||
err := json.NewEncoder(w).Encode(data)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error().Err(err).Msg("json encode error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) error { // nolint
|
|
||||||
return json.NewDecoder(r.Body).Decode(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
s.router.ServeHTTP(w, r)
|
s.router.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,269 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
"github.com/go-chi/chi"
|
|
||||||
|
|
||||||
// spooky action at a distance!
|
|
||||||
// this populates the environment
|
|
||||||
// from a ./.env file automatically
|
|
||||||
// for development configuration.
|
|
||||||
// .env contents should be things like
|
|
||||||
// `DBURL=postgres://user:pass@.../`
|
|
||||||
// (without the backticks, of course)
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
|
||||||
)
|
|
||||||
|
|
||||||
type server struct {
|
|
||||||
appname string
|
|
||||||
version string
|
|
||||||
buildarch string
|
|
||||||
databaseURL string
|
|
||||||
startupTime time.Time
|
|
||||||
port int
|
|
||||||
exitCode int
|
|
||||||
sentryEnabled bool
|
|
||||||
log *zerolog.Logger
|
|
||||||
ctx context.Context
|
|
||||||
cancelFunc context.CancelFunc
|
|
||||||
httpServer *http.Server
|
|
||||||
router *chi.Mux
|
|
||||||
}
|
|
||||||
|
|
||||||
func newServer(options ...func(s *server)) *server {
|
|
||||||
n := new(server)
|
|
||||||
n.startupTime = time.Now()
|
|
||||||
n.version = "unknown"
|
|
||||||
for _, opt := range options {
|
|
||||||
opt(n)
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME change this to use uber/fx DI and an Invoke()
|
|
||||||
// this is where we come in from package main.
|
|
||||||
func Run(appname, version, buildarch string) int {
|
|
||||||
s := newServer(func(i *server) {
|
|
||||||
i.appname = appname
|
|
||||||
if version != "" {
|
|
||||||
i.version = version
|
|
||||||
}
|
|
||||||
i.buildarch = buildarch
|
|
||||||
})
|
|
||||||
|
|
||||||
// this does nothing if SENTRY_DSN is unset in env.
|
|
||||||
|
|
||||||
// TODO remove:
|
|
||||||
if s.sentryEnabled {
|
|
||||||
sentry.CaptureMessage("It works!")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.configure()
|
|
||||||
s.setupLogging()
|
|
||||||
|
|
||||||
// logging before sentry, because sentry logs
|
|
||||||
s.enableSentry()
|
|
||||||
|
|
||||||
s.databaseURL = viper.GetString("DBURL")
|
|
||||||
s.port = viper.GetInt("PORT")
|
|
||||||
|
|
||||||
return s.serve()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) enableSentry() {
|
|
||||||
s.sentryEnabled = false
|
|
||||||
|
|
||||||
if viper.GetString("SENTRY_DSN") == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := sentry.Init(sentry.ClientOptions{
|
|
||||||
Dsn: viper.GetString("SENTRY_DSN"),
|
|
||||||
Release: fmt.Sprintf("%s-%s", s.appname, s.version),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("sentry init failure")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.log.Info().Msg("sentry error reporting activated")
|
|
||||||
s.sentryEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) serve() int {
|
|
||||||
s.ctx, s.cancelFunc = context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
// signal watcher
|
|
||||||
go func() {
|
|
||||||
c := make(chan os.Signal, 1)
|
|
||||||
signal.Ignore(syscall.SIGPIPE)
|
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
||||||
// block and wait for signal
|
|
||||||
sig := <-c
|
|
||||||
s.log.Info().Msgf("signal received: %+v", sig)
|
|
||||||
if s.cancelFunc != nil {
|
|
||||||
// cancelling the main context will trigger a clean
|
|
||||||
// shutdown.
|
|
||||||
s.cancelFunc()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go s.serveUntilShutdown()
|
|
||||||
|
|
||||||
for range s.ctx.Done() {
|
|
||||||
// aforementioned clean shutdown upon main context
|
|
||||||
// cancellation
|
|
||||||
}
|
|
||||||
s.cleanShutdown()
|
|
||||||
return s.exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) cleanupForExit() {
|
|
||||||
s.log.Info().Msg("cleaning up")
|
|
||||||
// FIXME unimplemented
|
|
||||||
// close database connections or whatever
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) cleanShutdown() {
|
|
||||||
// initiate clean shutdown
|
|
||||||
s.exitCode = 0
|
|
||||||
ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
if err := s.httpServer.Shutdown(ctxShutdown); err != nil {
|
|
||||||
s.log.Error().
|
|
||||||
Err(err).
|
|
||||||
Msg("server clean shutdown failed")
|
|
||||||
}
|
|
||||||
if shutdownCancel != nil {
|
|
||||||
shutdownCancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.cleanupForExit()
|
|
||||||
|
|
||||||
if s.sentryEnabled {
|
|
||||||
sentry.Flush(2 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) uptime() time.Duration {
|
|
||||||
return time.Since(s.startupTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) maintenance() bool {
|
|
||||||
return viper.GetBool("MAINTENANCE_MODE")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) configure() {
|
|
||||||
viper.SetConfigName(s.appname)
|
|
||||||
viper.SetConfigType("yaml")
|
|
||||||
// path to look for the config file in:
|
|
||||||
viper.AddConfigPath(fmt.Sprintf("/etc/%s", s.appname))
|
|
||||||
// call multiple times to add many search paths:
|
|
||||||
viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", s.appname))
|
|
||||||
// viper.SetEnvPrefix(strings.ToUpper(s.appname))
|
|
||||||
viper.AutomaticEnv()
|
|
||||||
|
|
||||||
viper.SetDefault("DEBUG", "false")
|
|
||||||
viper.SetDefault("MAINTENANCE_MODE", "false")
|
|
||||||
viper.SetDefault("PORT", "8080")
|
|
||||||
viper.SetDefault("DBURL", "")
|
|
||||||
viper.SetDefault("SENTRY_DSN", "")
|
|
||||||
viper.SetDefault("METRICS_USERNAME", "")
|
|
||||||
viper.SetDefault("METRICS_PASSWORD", "")
|
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
|
||||||
// Config file not found; ignore error if desired
|
|
||||||
} else {
|
|
||||||
// Config file was found but another error was produced
|
|
||||||
log.Panic().
|
|
||||||
Err(err).
|
|
||||||
Msg("config file malformed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if viper.GetBool("DEBUG") {
|
|
||||||
// pp.Print(viper.AllSettings())
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) setupLogging() {
|
|
||||||
// always log in UTC
|
|
||||||
zerolog.TimestampFunc = func() time.Time {
|
|
||||||
return time.Now().UTC()
|
|
||||||
}
|
|
||||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
|
||||||
|
|
||||||
tty := false
|
|
||||||
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
|
||||||
tty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var writers []io.Writer
|
|
||||||
|
|
||||||
if tty {
|
|
||||||
// this does cool colorization for console/dev
|
|
||||||
consoleWriter := zerolog.NewConsoleWriter(
|
|
||||||
func(w *zerolog.ConsoleWriter) {
|
|
||||||
// Customize time format
|
|
||||||
w.TimeFormat = time.RFC3339Nano
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
writers = append(writers, consoleWriter)
|
|
||||||
} else {
|
|
||||||
// log json in prod for the machines
|
|
||||||
writers = append(writers, os.Stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
// this is how you log to a file, if you do that
|
|
||||||
// sort of thing still
|
|
||||||
logfile := viper.GetString("Logfile")
|
|
||||||
if logfile != "" {
|
|
||||||
logfileDir := filepath.Dir(logfile)
|
|
||||||
err := goutil.Mkdirp(logfileDir)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("unable to create log dir")
|
|
||||||
}
|
|
||||||
|
|
||||||
hp.logfh, err = os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
|
||||||
if err != nil {
|
|
||||||
panic("unable to open logfile: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
writers = append(writers, hp.logfh)
|
|
||||||
*/
|
|
||||||
|
|
||||||
multi := zerolog.MultiLevelWriter(writers...)
|
|
||||||
logger := zerolog.New(multi).With().Timestamp().Logger().With().Caller().Logger()
|
|
||||||
|
|
||||||
s.log = &logger
|
|
||||||
// log.Logger = logger
|
|
||||||
|
|
||||||
if viper.GetBool("debug") {
|
|
||||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
||||||
s.log.Debug().Bool("debug", true).Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.identify()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) identify() {
|
|
||||||
s.log.Info().
|
|
||||||
Str("appname", s.appname).
|
|
||||||
Str("version", s.version).
|
|
||||||
Str("buildarch", s.buildarch).
|
|
||||||
Msg("starting")
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *server) routes() {
|
func (s *Server) SetupRoutes() {
|
||||||
s.router = chi.NewRouter()
|
s.router = chi.NewRouter()
|
||||||
|
|
||||||
// the mux .Use() takes a http.Handler wrapper func, like most
|
// the mux .Use() takes a http.Handler wrapper func, like most
|
||||||
@@ -23,16 +23,16 @@ func (s *server) routes() {
|
|||||||
|
|
||||||
s.router.Use(middleware.Recoverer)
|
s.router.Use(middleware.Recoverer)
|
||||||
s.router.Use(middleware.RequestID)
|
s.router.Use(middleware.RequestID)
|
||||||
s.router.Use(s.LoggingMiddleware())
|
s.router.Use(s.mw.Logging())
|
||||||
|
|
||||||
// add metrics middleware only if we can serve them behind auth
|
// add metrics middleware only if we can serve them behind auth
|
||||||
if viper.GetString("METRICS_USERNAME") != "" {
|
if viper.GetString("METRICS_USERNAME") != "" {
|
||||||
s.router.Use(s.MetricsMiddleware())
|
s.router.Use(s.mw.Metrics())
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up CORS headers. you'll probably want to configure that
|
// set up CORS headers. you'll probably want to configure that
|
||||||
// in middlewares.go.
|
// in middlewares.go.
|
||||||
s.router.Use(s.CORSMiddleware())
|
s.router.Use(s.mw.CORS())
|
||||||
|
|
||||||
// CHANGEME to suit your needs, or pull from config.
|
// CHANGEME to suit your needs, or pull from config.
|
||||||
// timeout for request context; your handlers must finish within
|
// timeout for request context; your handlers must finish within
|
||||||
@@ -57,39 +57,55 @@ func (s *server) routes() {
|
|||||||
// complete docs: https://github.com/go-chi/chi
|
// complete docs: https://github.com/go-chi/chi
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
s.router.Get("/", s.handleIndex())
|
s.router.Get("/", s.h.HandleIndex())
|
||||||
|
|
||||||
s.router.Mount("/s", http.StripPrefix("/s", http.FileServer(http.FS(static.Static))))
|
s.router.Mount("/s", http.StripPrefix("/s", http.FileServer(http.FS(static.Static))))
|
||||||
|
|
||||||
s.router.Route("/api/v1", func(r chi.Router) {
|
s.router.Route("/api/v1", func(r chi.Router) {
|
||||||
r.Get("/now", s.handleNow())
|
r.Get("/now", s.h.HandleNow())
|
||||||
|
|
||||||
|
// Posts CRUD
|
||||||
|
r.Get("/posts", s.h.HandleListPosts())
|
||||||
|
r.Post("/posts", s.h.HandleCreatePost())
|
||||||
|
r.Get("/posts/{id}", s.h.HandleGetPost())
|
||||||
|
r.Put("/posts/{id}", s.h.HandleUpdatePost())
|
||||||
|
r.Delete("/posts/{id}", s.h.HandleDeletePost())
|
||||||
})
|
})
|
||||||
|
|
||||||
// if you want to use a general purpose middleware (http.Handler
|
// if you want to use a general purpose middleware (http.Handler
|
||||||
// wrapper) on a specific HandleFunc route, you need to take the
|
// wrapper) on a specific HandleFunc route, you need to take the
|
||||||
// .ServeHTTP of the http.Handler to get its HandleFunc, viz:
|
// .ServeHTTP of the http.Handler to get its HandleFunc, viz:
|
||||||
authMiddleware := s.AuthMiddleware()
|
auth := s.mw.Auth()
|
||||||
s.router.Get(
|
s.router.Get(
|
||||||
"/login",
|
"/login",
|
||||||
authMiddleware(s.handleLogin()).ServeHTTP,
|
auth(s.h.HandleLoginGET()).ServeHTTP,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
s.router.Get(
|
||||||
|
"/signup",
|
||||||
|
auth(s.h.HandleSignupGET()).ServeHTTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
s.router.Post(
|
||||||
|
"/signup",
|
||||||
|
auth(s.h.HandleSignupPOST()).ServeHTTP,
|
||||||
|
)
|
||||||
// route that panics for testing
|
// route that panics for testing
|
||||||
// CHANGEME remove this
|
// CHANGEME remove this
|
||||||
s.router.Get(
|
s.router.Get(
|
||||||
"/panic",
|
"/panic",
|
||||||
s.handlePanic(),
|
s.h.HandlePanic(),
|
||||||
)
|
)
|
||||||
|
|
||||||
s.router.Get(
|
s.router.Get(
|
||||||
"/.well-known/healthcheck.json",
|
"/.well-known/healthcheck.json",
|
||||||
s.handleHealthCheck(),
|
s.h.HandleHealthCheck(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// set up authenticated /metrics route:
|
// set up authenticated /metrics route:
|
||||||
if viper.GetString("METRICS_USERNAME") != "" {
|
if viper.GetString("METRICS_USERNAME") != "" {
|
||||||
s.router.Group(func(r chi.Router) {
|
s.router.Group(func(r chi.Router) {
|
||||||
r.Use(s.MetricsAuthMiddleware())
|
r.Use(s.mw.MetricsAuth())
|
||||||
r.Get("/metrics", http.HandlerFunc(promhttp.Handler().ServeHTTP))
|
r.Get("/metrics", http.HandlerFunc(promhttp.Handler().ServeHTTP))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
175
internal/server/server.go
Normal file
175
internal/server/server.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/config"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/globals"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/handlers"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/logger"
|
||||||
|
"git.eeqj.de/sneak/gohttpserver/internal/middleware"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
|
||||||
|
// spooky action at a distance!
|
||||||
|
// this populates the environment
|
||||||
|
// from a ./.env file automatically
|
||||||
|
// for development configuration.
|
||||||
|
// .env contents should be things like
|
||||||
|
// `DBURL=postgres://user:pass@.../`
|
||||||
|
// (without the backticks, of course)
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerParams struct {
|
||||||
|
fx.In
|
||||||
|
Logger *logger.Logger
|
||||||
|
Globals *globals.Globals
|
||||||
|
Config *config.Config
|
||||||
|
Middleware *middleware.Middleware
|
||||||
|
Handlers *handlers.Handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
startupTime time.Time
|
||||||
|
exitCode int
|
||||||
|
sentryEnabled bool
|
||||||
|
log *slog.Logger
|
||||||
|
ctx context.Context
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
httpServer *http.Server
|
||||||
|
router *chi.Mux
|
||||||
|
params ServerParams
|
||||||
|
mw *middleware.Middleware
|
||||||
|
h *handlers.Handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
|
||||||
|
s := new(Server)
|
||||||
|
s.params = params
|
||||||
|
s.mw = params.Middleware
|
||||||
|
s.h = params.Handlers
|
||||||
|
s.log = params.Logger.Get()
|
||||||
|
|
||||||
|
lc.Append(fx.Hook{
|
||||||
|
OnStart: func(ctx context.Context) error {
|
||||||
|
s.startupTime = time.Now()
|
||||||
|
go s.Run() // background FIXME
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnStop: func(ctx context.Context) error {
|
||||||
|
// FIXME do server shutdown here
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME change this to use uber/fx DI and an Invoke()
|
||||||
|
// this is where we come in from package main.
|
||||||
|
func (s *Server) Run() {
|
||||||
|
// this does nothing if SENTRY_DSN is unset in env.
|
||||||
|
// TODO remove:
|
||||||
|
if s.sentryEnabled {
|
||||||
|
sentry.CaptureMessage("It works!")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.configure()
|
||||||
|
|
||||||
|
// logging before sentry, because sentry logs
|
||||||
|
s.enableSentry()
|
||||||
|
|
||||||
|
s.serve() // FIXME deal with return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) enableSentry() {
|
||||||
|
s.sentryEnabled = false
|
||||||
|
|
||||||
|
if s.params.Config.SentryDSN == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: s.params.Config.SentryDSN,
|
||||||
|
Release: fmt.Sprintf("%s-%s", s.params.Globals.Appname, s.params.Globals.Version),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("sentry init failure", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
s.log.Info("sentry error reporting activated")
|
||||||
|
s.sentryEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) serve() int {
|
||||||
|
// FIXME fx will handle this for us
|
||||||
|
s.ctx, s.cancelFunc = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// signal watcher
|
||||||
|
go func() {
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Ignore(syscall.SIGPIPE)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
// block and wait for signal
|
||||||
|
sig := <-c
|
||||||
|
s.log.Info("signal received", "signal", sig)
|
||||||
|
if s.cancelFunc != nil {
|
||||||
|
// cancelling the main context will trigger a clean
|
||||||
|
// shutdown.
|
||||||
|
s.cancelFunc()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go s.serveUntilShutdown()
|
||||||
|
|
||||||
|
for range s.ctx.Done() {
|
||||||
|
// aforementioned clean shutdown upon main context
|
||||||
|
// cancellation
|
||||||
|
}
|
||||||
|
s.cleanShutdown()
|
||||||
|
return s.exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) cleanupForExit() {
|
||||||
|
s.log.Info("cleaning up")
|
||||||
|
// FIXME unimplemented
|
||||||
|
// close database connections or whatever
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) cleanShutdown() {
|
||||||
|
// initiate clean shutdown
|
||||||
|
s.exitCode = 0
|
||||||
|
ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
if err := s.httpServer.Shutdown(ctxShutdown); err != nil {
|
||||||
|
s.log.Error("server clean shutdown failed", "error", err)
|
||||||
|
}
|
||||||
|
if shutdownCancel != nil {
|
||||||
|
shutdownCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.cleanupForExit()
|
||||||
|
|
||||||
|
if s.sentryEnabled {
|
||||||
|
sentry.Flush(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) MaintenanceMode() bool {
|
||||||
|
return s.params.Config.MaintenanceMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) configure() {
|
||||||
|
// FIXME move most of this to dedicated places
|
||||||
|
// if viper.GetBool("DEBUG") {
|
||||||
|
// pp.Print(viper.AllSettings())
|
||||||
|
// }
|
||||||
|
}
|
||||||
3
templates/htmlfooter.html
Normal file
3
templates/htmlfooter.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<script src="/s/js/jquery-3.5.1.slim.min.js"></script>
|
||||||
|
<script src="/s/js/bootstrap-4.5.3.bundle.min.js"></script>
|
||||||
|
<script src="/s/js/main.js"></script>
|
||||||
4
templates/htmlheader.html
Normal file
4
templates/htmlheader.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>{{ .HTMLTitle }}</title>
|
||||||
|
<link rel="stylesheet" href="/s/css/bootstrap-4.5.3.min.css" />
|
||||||
|
<link rel="stylesheet" href="/s/css/style.css" />
|
||||||
@@ -1,84 +1,29 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
{{ template "htmlheader.html" . }}
|
||||||
<title>Changeme: Go HTTP Server Boilerplate</title>
|
|
||||||
<link rel="stylesheet" href="/s/css/bootstrap-4.5.3.min.css" />
|
|
||||||
<link rel="stylesheet" href="/s/css/style.css" />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
{{ template "navbar.html" .}}
|
||||||
<a class="navbar-brand" href="#">Quickstart</a>
|
|
||||||
<button
|
|
||||||
class="navbar-toggler"
|
|
||||||
type="button"
|
|
||||||
data-toggle="collapse"
|
|
||||||
data-target="#navbarsExampleDefault"
|
|
||||||
aria-controls="navbarsExampleDefault"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-label="Toggle navigation"
|
|
||||||
>
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
|
||||||
<ul class="navbar-nav mr-auto">
|
|
||||||
<li class="nav-item active">
|
|
||||||
<a class="nav-link" href="#"
|
|
||||||
>Home <span class="sr-only">(current)</span></a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="#">Link</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a
|
|
||||||
class="nav-link disabled"
|
|
||||||
href="#"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-disabled="true"
|
|
||||||
>Disabled</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a
|
|
||||||
class="nav-link dropdown-toggle"
|
|
||||||
href="#"
|
|
||||||
id="dropdown01"
|
|
||||||
data-toggle="dropdown"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="false"
|
|
||||||
>Dropdown</a
|
|
||||||
>
|
|
||||||
<div class="dropdown-menu" aria-labelledby="dropdown01">
|
|
||||||
<a class="dropdown-item" href="#">Action</a>
|
|
||||||
<a class="dropdown-item" href="#">Another action</a>
|
|
||||||
<a class="dropdown-item" href="#">Something else here</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<form class="form-inline my-2 my-lg-0">
|
|
||||||
<input
|
|
||||||
class="form-control mr-sm-2"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search"
|
|
||||||
aria-label="Search"
|
|
||||||
/>
|
|
||||||
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<main role="main">
|
<main role="main">
|
||||||
<div class="jumbotron">
|
<div class="jumbotron">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="display-3">Hello, world!</h1>
|
<h1 class="display-3">Hello, world!</h1>
|
||||||
|
<h2><a
|
||||||
|
href="https://git.eeqj.de/sneak/gohttpserver">gohttpserver</a></h2>
|
||||||
<p>
|
<p>
|
||||||
This is a boilerplate application for you to use as a base for your
|
This is a boilerplate application for you to use as a base for your
|
||||||
own sites and services.
|
own sites and services.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
Find more info at <a
|
||||||
|
href="https://git.eeqj.de/sneak/gohttpserver">https://git.eeqj.de/sneak/gohttpserver</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This software is provided by <a
|
||||||
|
href="https://sneak.berlin">@sneak</a>
|
||||||
|
and is released unconditionally into the public domain.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a class="btn btn-primary btn-lg" href="#" role="button"
|
<a class="btn btn-primary btn-lg" href="#" role="button"
|
||||||
>Learn more »</a
|
>Learn more »</a
|
||||||
@@ -130,11 +75,7 @@
|
|||||||
<!-- /container -->
|
<!-- /container -->
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="container">
|
{{ template "pagefooter.html" . }}
|
||||||
<p>© No rights reserved - This is in the public domain!</p>
|
|
||||||
</footer>
|
|
||||||
<script src="/s/js/jquery-3.5.1.slim.min.js"></script>
|
|
||||||
<script src="/s/js/bootstrap-4.5.3.bundle.min.js"></script>
|
|
||||||
<script src="/s/js/main.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
{{ template "htmlfooter.html" . }}
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
{{ template "htmlheader.html" . }}
|
||||||
<title>Changeme: Go HTTP Server Boilerplate</title>
|
|
||||||
<link rel="stylesheet" href="/s/css/bootstrap-4.5.3.min.css" />
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
padding-top: 3.5rem;
|
padding-top: 3.5rem;
|
||||||
@@ -28,73 +25,9 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
{{ template "navbar.html" .}}
|
||||||
<a class="navbar-brand" href="#">Quickstart</a>
|
|
||||||
<button
|
|
||||||
class="navbar-toggler"
|
|
||||||
type="button"
|
|
||||||
data-toggle="collapse"
|
|
||||||
data-target="#navbarsExampleDefault"
|
|
||||||
aria-controls="navbarsExampleDefault"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-label="Toggle navigation"
|
|
||||||
>
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
|
||||||
<ul class="navbar-nav mr-auto">
|
|
||||||
<li class="nav-item active">
|
|
||||||
<a class="nav-link" href="#"
|
|
||||||
>Home <span class="sr-only">(current)</span></a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="#">Link</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a
|
|
||||||
class="nav-link disabled"
|
|
||||||
href="#"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-disabled="true"
|
|
||||||
>Disabled</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a
|
|
||||||
class="nav-link dropdown-toggle"
|
|
||||||
href="#"
|
|
||||||
id="dropdown01"
|
|
||||||
data-toggle="dropdown"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="false"
|
|
||||||
>Dropdown</a
|
|
||||||
>
|
|
||||||
<div class="dropdown-menu" aria-labelledby="dropdown01">
|
|
||||||
<a class="dropdown-item" href="#">Action</a>
|
|
||||||
<a class="dropdown-item" href="#">Another action</a>
|
|
||||||
<a class="dropdown-item" href="#">Something else here</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<form class="form-inline my-2 my-lg-0">
|
|
||||||
<input
|
|
||||||
class="form-control mr-sm-2"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search"
|
|
||||||
aria-label="Search"
|
|
||||||
/>
|
|
||||||
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<main role="main">
|
<main role="main">
|
||||||
|
|
||||||
|
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<form action="/login" method="post">
|
<form action="/login" method="post">
|
||||||
<h2 class="text-center">Log in</h2>
|
<h2 class="text-center">Log in</h2>
|
||||||
@@ -118,11 +51,6 @@
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="container">
|
{{ template "pagefooter.html" . }}
|
||||||
<p>© No rights reserved - This is in the public domain!</p>
|
|
||||||
</footer>
|
|
||||||
<script src="/s/js/jquery-3.5.1.slim.min.js"></script>
|
|
||||||
<script src="/s/js/bootstrap-4.5.3.bundle.min.js"></script>
|
|
||||||
<script src="/s/js/main.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
64
templates/navbar.html
Normal file
64
templates/navbar.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||||
|
<a class="navbar-brand" href="/">{{.SiteName}}</a>
|
||||||
|
<button
|
||||||
|
class="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-toggle="collapse"
|
||||||
|
data-target="#navbarsExampleDefault"
|
||||||
|
aria-controls="navbarsExampleDefault"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<span class="navbar-toggler-icon"> </span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
||||||
|
<ul class="navbar-nav mr-auto">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a class="nav-link" href="#"
|
||||||
|
>Home <span class="sr-only">(current)</span></a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Link</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link disabled" href="#" aria-disabled="true"
|
||||||
|
>Disabled</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
href="#"
|
||||||
|
id="dropdown01"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false"
|
||||||
|
>Dropdown</a
|
||||||
|
>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="dropdown01">
|
||||||
|
<a class="dropdown-item" href="#">Action</a>
|
||||||
|
<a class="dropdown-item" href="#">Another action</a>
|
||||||
|
<a class="dropdown-item" href="#"
|
||||||
|
>Something else here</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form action="POST" class="form-inline my-2 my-lg-0">
|
||||||
|
<input
|
||||||
|
class="form-control mr-sm-2"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
aria-label="Search"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-success my-2 my-sm-0"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
3
templates/pagefooter.html
Normal file
3
templates/pagefooter.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<footer class="container">
|
||||||
|
<p>© No rights reserved - This is in the public domain!</p>
|
||||||
|
</footer>
|
||||||
76
templates/signup.html
Normal file
76
templates/signup.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{ template "htmlheader.html" .HTMLHeader }}
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding-top: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-placeholder-img {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
text-anchor: middle;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.bd-placeholder-img-lg {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{ template "navbar.html" .Navbar}}
|
||||||
|
|
||||||
|
<main role="main">
|
||||||
|
<div class="signup-form">
|
||||||
|
<form action="/signup" method="post">
|
||||||
|
<h2 class="text-center">Create New Account</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<span>Email:</span>
|
||||||
|
<input type="text" class="form-control" name="email"
|
||||||
|
placeholder="user@domain.com" required="required">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<span>Desired Username:</span>
|
||||||
|
<input type="text" class="form-control" name="desiredUsername" placeholder="Username" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<p>Please use a unique password that you don't use anywhere
|
||||||
|
else. Minimum 12 characters.</p>
|
||||||
|
<span>New Password:</span>
|
||||||
|
<input type="password" class="form-control"
|
||||||
|
name="desiredPassword1" placeholder="Password" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span>New Password (again):</span>
|
||||||
|
<input type="password" class="form-control"
|
||||||
|
name="desiredPassword2" placeholder="Password
|
||||||
|
(again)" required="required">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Create
|
||||||
|
New Account</button>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="clearfix">
|
||||||
|
<label class="float-left form-check-label"><input type="checkbox"> Remember me</label>
|
||||||
|
<a href="#" class="float-right">Forgot Password?</a>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</form>
|
||||||
|
<p class="text-center"><a href="#">Create an Account</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{{ template "pagefooter.html" .PageFooter }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,12 +2,21 @@ package templates
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"strings"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed *.html
|
//go:embed *.html
|
||||||
var Templates embed.FS
|
var TemplatesRaw embed.FS
|
||||||
|
var TemplatesParsed *template.Template
|
||||||
|
|
||||||
|
func GetParsed() *template.Template {
|
||||||
|
if TemplatesParsed == nil {
|
||||||
|
TemplatesParsed = template.Must(template.ParseFS(TemplatesRaw, "*"))
|
||||||
|
}
|
||||||
|
return TemplatesParsed
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
func MustString(filename string) string {
|
func MustString(filename string) string {
|
||||||
bytes, error := Templates.ReadFile(filename)
|
bytes, error := Templates.ReadFile(filename)
|
||||||
if error != nil {
|
if error != nil {
|
||||||
@@ -17,3 +26,4 @@ func MustString(filename string) string {
|
|||||||
out.Write(bytes)
|
out.Write(bytes)
|
||||||
return out.String()
|
return out.String()
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user