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
|
||||
name: test-docker-build
|
||||
|
||||
|
||||
steps:
|
||||
- name: test-docker-build
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: foo/bar
|
||||
dry_run: true
|
||||
target: final
|
||||
tags:
|
||||
- ${DRONE_COMMIT_SHA}
|
||||
- ${DRONE_BRANCH}
|
||||
- name: test
|
||||
image: docker:dind
|
||||
volumes:
|
||||
- name: dockersock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
#- echo nameserver 116.202.204.30 > /etc/resolv.conf
|
||||
- docker build -t sneak/gohttpserver .
|
||||
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
|
||||
debug.log
|
||||
/.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
|
||||
FROM golangci/golangci-lint@sha256:9ae3767101cd3468cdaea5b6573dadb358013e05ac38abe37d53646680fd386c AS linter
|
||||
|
||||
RUN mkdir -p /build
|
||||
WORKDIR /build
|
||||
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 .
|
||||
# Lint stage — fast feedback
|
||||
# golangci/golangci-lint:v1.64.8 (2025-03-17)
|
||||
FROM golangci/golangci-lint@sha256:2987913e27f4eca9c8a39129d2c7bc1e74fbcf77f181e01cea607be437aa5cb8 AS lint
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN make fmt-check
|
||||
RUN make lint
|
||||
|
||||
COPY ./ ./
|
||||
#RUN make lint
|
||||
RUN make rice-install && make httpd
|
||||
RUN go mod vendor
|
||||
RUN tar -c . | bzip2 > /src.tbz2
|
||||
# Build stage
|
||||
# golang:1.24-bookworm (Go 1.24)
|
||||
FROM golang@sha256:1a6d4452c65dea36aac2e2d606b01b4a029ec90cc1ae53890540ce6173ea77ac AS builder
|
||||
# Force BuildKit to run the lint stage
|
||||
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:
|
||||
FROM debian:buster-slim AS final
|
||||
# Runtime stage
|
||||
# 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 /build/httpd /app/httpd
|
||||
COPY --from=builder /src.tbz2 /usr/local/src/src.tbz2
|
||||
|
||||
COPY --from=builder /httpd /app/httpd
|
||||
|
||||
WORKDIR /app
|
||||
ENV HOME /app
|
||||
|
||||
ENV PORT 8080
|
||||
ENV DBURL none
|
||||
ENV HOME=/app
|
||||
ENV PORT=8080
|
||||
ENV DBURL=none
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
29
Makefile
29
Makefile
@@ -4,7 +4,6 @@ VERSION := $(shell git describe --always --dirty=-dirty)
|
||||
ARCH := $(shell uname -m)
|
||||
UNAME_S := $(shell uname -s)
|
||||
GOLDFLAGS += -X main.Version=$(VERSION)
|
||||
GOLDFLAGS += -X main.Buildarch=$(ARCH)
|
||||
GOFLAGS := -ldflags "$(GOLDFLAGS)"
|
||||
|
||||
default: clean debug
|
||||
@@ -12,17 +11,30 @@ default: clean debug
|
||||
commit: fmt lint
|
||||
git commit -a
|
||||
|
||||
# get golangci-lint with:
|
||||
# go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.31.0
|
||||
# get gofumports with:
|
||||
# go get mvdan.cc/gofumpt/gofumports
|
||||
# get gofumpt with:
|
||||
# go install mvdan.cc/gofumpt@latest
|
||||
fmt:
|
||||
gofumpt -l -w .
|
||||
golangci-lint run --fix
|
||||
|
||||
fmt-check:
|
||||
@test -z "$$(gofmt -l .)" || { echo "gofmt found unformatted files:"; gofmt -l .; exit 1; }
|
||||
|
||||
lint:
|
||||
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=1 GOTRACEBACK=all ./$(FN)d
|
||||
@@ -44,5 +56,6 @@ docker:
|
||||
go build -o ../../$(FN)d $(GOFLAGS) .
|
||||
|
||||
tools:
|
||||
go get -v github.com/golangci/golangci-lint/cmd/golangci-lint@v1.31.0
|
||||
go get -v mvdan.cc/gofumpt/gofumports
|
||||
go install mvdan.cc/gofumpt@latest
|
||||
|
||||
.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.
|
||||
```
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
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"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
var (
|
||||
Appname string = "CHANGEME"
|
||||
Version string
|
||||
Buildarch string
|
||||
Appname string = "CHANGEME"
|
||||
Version string
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
go 1.19
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
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/joho/godotenv v1.4.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/spf13/viper v1.14.0
|
||||
go.uber.org/fx v1.18.2
|
||||
modernc.org/sqlite v1.41.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // 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/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // 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.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // 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/v2 v2.0.6 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.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/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // 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
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // 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-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
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-20200629203442-efcf912fb354/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
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/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/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
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/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
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/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=
|
||||
@@ -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.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
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/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=
|
||||
@@ -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.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.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/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=
|
||||
@@ -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-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-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/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.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
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/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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
|
||||
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-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/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/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/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/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/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
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.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
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.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
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.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.32/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.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
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.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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-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-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-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
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.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.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-20180826012351-8a410e7b638d/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-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-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-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-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-20190226205417-e64efc72b421/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-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-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-20180905080454-ebe1bf3edb33/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-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-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-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-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-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.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
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-20191011141410-1b5146add898/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.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.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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
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-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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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-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=
|
||||
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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
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 (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *server) handleNow() http.HandlerFunc {
|
||||
func (s *Handlers) HandleNow() http.HandlerFunc {
|
||||
type response struct {
|
||||
Now time.Time `json:"now"`
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package server
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// CHANGEME you probably want to remove this,
|
||||
// this is just a handler/route that throws a panic to test
|
||||
// sentry events.
|
||||
func (s *server) handlePanic() http.HandlerFunc {
|
||||
func (s *Handlers) HandlePanic() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
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 (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"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"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
@@ -12,10 +16,27 @@ import (
|
||||
ghmm "github.com/slok/go-http-metrics/middleware"
|
||||
"github.com/slok/go-http-metrics/middleware/std"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
// the following is from
|
||||
// https://learning-cloud-native-go.github.io/docs/a6.adding_zerolog_logger/
|
||||
type MiddlewareParams struct {
|
||||
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 {
|
||||
h, _, err := net.SplitHostPort(hp)
|
||||
@@ -45,8 +66,7 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
||||
// type Middleware func(http.Handler) http.Handler
|
||||
// this returns a Middleware that is designed to do every request through the
|
||||
// mux, note the signature:
|
||||
func (s *server) LoggingMiddleware() func(http.Handler) http.Handler {
|
||||
// FIXME this should use https://github.com/google/go-cloud/blob/master/server/requestlog/requestlog.go
|
||||
func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
@@ -54,18 +74,18 @@ func (s *server) LoggingMiddleware() func(http.Handler) http.Handler {
|
||||
ctx := r.Context()
|
||||
defer func() {
|
||||
latency := time.Since(start)
|
||||
s.log.Info().
|
||||
Time("request_start", start).
|
||||
Str("method", r.Method).
|
||||
Str("url", r.URL.String()).
|
||||
Str("useragent", r.UserAgent()).
|
||||
Str("request_id", ctx.Value(middleware.RequestIDKey).(string)).
|
||||
Str("referer", r.Referer()).
|
||||
Str("proto", r.Proto).
|
||||
Str("remoteIP", ipFromHostPort(r.RemoteAddr)).
|
||||
Int("status", lrw.statusCode).
|
||||
Int("latency_ms", int(latency.Milliseconds())).
|
||||
Send()
|
||||
s.log.InfoContext(ctx, "request",
|
||||
"request_start", start,
|
||||
"method", r.Method,
|
||||
"url", r.URL.String(),
|
||||
"useragent", r.UserAgent(),
|
||||
"request_id", ctx.Value(middleware.RequestIDKey).(string),
|
||||
"referer", r.Referer(),
|
||||
"proto", r.Proto,
|
||||
"remoteIP", ipFromHostPort(r.RemoteAddr),
|
||||
"status", lrw.statusCode,
|
||||
"latency_ms", latency.Milliseconds(),
|
||||
)
|
||||
}()
|
||||
|
||||
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{
|
||||
// CHANGEME! these are defaults, change them to suit your needs or
|
||||
// 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 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) MetricsMiddleware() func(http.Handler) http.Handler {
|
||||
func (s *Middleware) Metrics() func(http.Handler) http.Handler {
|
||||
mdlw := ghmm.New(ghmm.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(
|
||||
"metrics",
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *server) serveUntilShutdown() {
|
||||
listenAddr := fmt.Sprintf(":%d", s.port)
|
||||
func (s *Server) serveUntilShutdown() {
|
||||
listenAddr := fmt.Sprintf(":%d", s.params.Config.Port)
|
||||
s.httpServer = &http.Server{
|
||||
Addr: listenAddr,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
@@ -19,32 +18,17 @@ func (s *server) serveUntilShutdown() {
|
||||
|
||||
// add routes
|
||||
// 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 {
|
||||
s.log.Error().Msgf("listen:%+s\n", err)
|
||||
s.log.Error("listen error", "error", err)
|
||||
if s.cancelFunc != nil {
|
||||
s.cancelFunc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) 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().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) {
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
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"
|
||||
)
|
||||
|
||||
func (s *server) routes() {
|
||||
func (s *Server) SetupRoutes() {
|
||||
s.router = chi.NewRouter()
|
||||
|
||||
// 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.RequestID)
|
||||
s.router.Use(s.LoggingMiddleware())
|
||||
s.router.Use(s.mw.Logging())
|
||||
|
||||
// add metrics middleware only if we can serve them behind auth
|
||||
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
|
||||
// in middlewares.go.
|
||||
s.router.Use(s.CORSMiddleware())
|
||||
s.router.Use(s.mw.CORS())
|
||||
|
||||
// CHANGEME to suit your needs, or pull from config.
|
||||
// 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
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
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.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
|
||||
// wrapper) on a specific HandleFunc route, you need to take the
|
||||
// .ServeHTTP of the http.Handler to get its HandleFunc, viz:
|
||||
authMiddleware := s.AuthMiddleware()
|
||||
auth := s.mw.Auth()
|
||||
s.router.Get(
|
||||
"/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
|
||||
// CHANGEME remove this
|
||||
s.router.Get(
|
||||
"/panic",
|
||||
s.handlePanic(),
|
||||
s.h.HandlePanic(),
|
||||
)
|
||||
|
||||
s.router.Get(
|
||||
"/.well-known/healthcheck.json",
|
||||
s.handleHealthCheck(),
|
||||
s.h.HandleHealthCheck(),
|
||||
)
|
||||
|
||||
// set up authenticated /metrics route:
|
||||
if viper.GetString("METRICS_USERNAME") != "" {
|
||||
s.router.Group(func(r chi.Router) {
|
||||
r.Use(s.MetricsAuthMiddleware())
|
||||
r.Use(s.mw.MetricsAuth())
|
||||
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>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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" />
|
||||
{{ template "htmlheader.html" . }}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<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>
|
||||
|
||||
{{ template "navbar.html" .}}
|
||||
<main role="main">
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1 class="display-3">Hello, world!</h1>
|
||||
<h2><a
|
||||
href="https://git.eeqj.de/sneak/gohttpserver">gohttpserver</a></h2>
|
||||
<p>
|
||||
This is a boilerplate application for you to use as a base for your
|
||||
own sites and services.
|
||||
</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>
|
||||
<a class="btn btn-primary btn-lg" href="#" role="button"
|
||||
>Learn more »</a
|
||||
@@ -130,11 +75,7 @@
|
||||
<!-- /container -->
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<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>
|
||||
{{ template "pagefooter.html" . }}
|
||||
</body>
|
||||
{{ template "htmlfooter.html" . }}
|
||||
</html>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Changeme: Go HTTP Server Boilerplate</title>
|
||||
<link rel="stylesheet" href="/s/css/bootstrap-4.5.3.min.css" />
|
||||
|
||||
{{ template "htmlheader.html" . }}
|
||||
<style>
|
||||
body {
|
||||
padding-top: 3.5rem;
|
||||
@@ -28,74 +25,10 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<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>
|
||||
{{ template "navbar.html" .}}
|
||||
|
||||
<main role="main">
|
||||
|
||||
|
||||
<div class="login-form">
|
||||
<div class="login-form">
|
||||
<form action="/login" method="post">
|
||||
<h2 class="text-center">Log in</h2>
|
||||
<div class="form-group">
|
||||
@@ -118,11 +51,6 @@
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="container">
|
||||
<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>
|
||||
{{ template "pagefooter.html" . }}
|
||||
</body>
|
||||
</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 (
|
||||
"embed"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//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 {
|
||||
bytes, error := Templates.ReadFile(filename)
|
||||
if error != nil {
|
||||
@@ -17,3 +26,4 @@ func MustString(filename string) string {
|
||||
out.Write(bytes)
|
||||
return out.String()
|
||||
}
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user