begin core module
This commit is contained in:
parent
e742fe876f
commit
89c39515f0
28 changed files with 666 additions and 0 deletions
18
.devcontainer/Dockerfile
Normal file
18
.devcontainer/Dockerfile
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
FROM debian:12
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ARG GO_VERSION
|
||||||
|
ARG GOLANGCI_LINT_VERSION
|
||||||
|
ARG MIGRATE_VERSION
|
||||||
|
|
||||||
|
RUN apt update &&\
|
||||||
|
apt install git wget curl -y &&\
|
||||||
|
wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz &&\
|
||||||
|
rm -rf /usr/local/go && tar -C /usr/local -xzf go$GO_VERSION.linux-amd64.tar.gz &&\
|
||||||
|
curl -sSfL https://raw.githubusercontent.com/air-verse/air/master/install.sh | sh -s -- -b /usr/local/go/bin &&\
|
||||||
|
wget https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.deb &&\
|
||||||
|
dpkg -i golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.deb &&\
|
||||||
|
wget https://github.com/golang-migrate/migrate/releases/download/v$MIGRATE_VERSION/migrate.linux-amd64.deb &&\
|
||||||
|
dpkg -i migrate.linux-amd64.deb &&\
|
||||||
|
echo "export PATH=$PATH:/usr/local/go/bin" > /root/.bashrc
|
22
.devcontainer/devcontainer.json
Normal file
22
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"workspaceFolder": "/workspace",
|
||||||
|
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,Z",
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile",
|
||||||
|
"args": {
|
||||||
|
"GO_VERSION": "1.24.0",
|
||||||
|
"GOLANGCI_LINT_VERSION": "1.64.5",
|
||||||
|
"MIGRATE_VERSION": "4.18.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"golang.go"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [
|
||||||
|
3000
|
||||||
|
]
|
||||||
|
}
|
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# dist directory
|
||||||
|
dist/
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"go.lintTool": "golangci-lint",
|
||||||
|
"go.lintFlags": [
|
||||||
|
"--fast"
|
||||||
|
]
|
||||||
|
}
|
17
.woodpecker/.build.yml
Normal file
17
.woodpecker/.build.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
when:
|
||||||
|
event: [tag, push]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
build:
|
||||||
|
image: golang:1.23-alpine
|
||||||
|
commands:
|
||||||
|
- apk update
|
||||||
|
- apk add git
|
||||||
|
- go install github.com/goreleaser/goreleaser/v2@latest
|
||||||
|
- echo "$${SENTRY_DSN}" > cmd/api/sentry
|
||||||
|
- goreleaser build --snapshot
|
||||||
|
secrets: [sentry_dsn]
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- lint
|
||||||
|
- tests
|
11
.woodpecker/.lint.yml
Normal file
11
.woodpecker/.lint.yml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
when:
|
||||||
|
event: [tag, push]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
lint:
|
||||||
|
image: golang:1.23-alpine
|
||||||
|
commands:
|
||||||
|
- apk update
|
||||||
|
- apk add bash curl jq
|
||||||
|
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.60.1
|
||||||
|
- $(go env GOPATH)/bin/golangci-lint run --fast
|
25
.woodpecker/.publish.yml
Normal file
25
.woodpecker/.publish.yml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
when:
|
||||||
|
event: [tag]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: publish-docker
|
||||||
|
image: docker:27-cli
|
||||||
|
commands:
|
||||||
|
- tag="tintounn/trepa:$${CI_COMMIT_TAG}"
|
||||||
|
- docker login -u="$${DOCKER_USERNAME}" -p="$${DOCKER_PASSWORD}"
|
||||||
|
- docker build --rm --tag $tag .
|
||||||
|
- docker push $tag
|
||||||
|
- docker rmi $tag
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
secrets: [docker_username, docker_password]
|
||||||
|
|
||||||
|
- name: publish-binaries
|
||||||
|
image: goreleaser/goreleaser
|
||||||
|
commands:
|
||||||
|
- goreleaser release
|
||||||
|
secrets: [ gitea_token ]
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
|
22
.woodpecker/.tests.yml
Normal file
22
.woodpecker/.tests.yml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
when:
|
||||||
|
event: [tag, push]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
tests:
|
||||||
|
image: golang:1.23-alpine
|
||||||
|
environment:
|
||||||
|
TEST_DATABASE_URL: postgres://dev:dev@db/trepa?sslmode=disable
|
||||||
|
commands:
|
||||||
|
- sleep 30
|
||||||
|
- wget https://github.com/golang-migrate/migrate/releases/download/v4.17.1/migrate.linux-amd64.tar.gz
|
||||||
|
- tar -xf migrate.linux-amd64.tar.gz
|
||||||
|
- ./migrate -source file://migrations/ -database "$${TEST_DATABASE_URL}" up
|
||||||
|
- go test -cover ./internal/... -v
|
||||||
|
|
||||||
|
services:
|
||||||
|
- name: db
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=dev
|
||||||
|
- POSTGRES_PASSWORD=dev
|
||||||
|
- POSTGRES_DB=trepa
|
33
Dockerfile
Normal file
33
Dockerfile
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
FROM golang:1.24 AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY cmd/ cmd/
|
||||||
|
COPY internal/ internal/
|
||||||
|
COPY go.mod .
|
||||||
|
COPY go.sum .
|
||||||
|
COPY scripts/ scripts/
|
||||||
|
|
||||||
|
RUN chmod +x scripts/build.sh && \
|
||||||
|
./scripts/build.sh
|
||||||
|
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt update &&\
|
||||||
|
apt install bash curl -y &&\
|
||||||
|
wget https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.deb &&\
|
||||||
|
dpkg -i migrate.linux-amd64.deb &&\
|
||||||
|
rm migrate.linux-amd64.deb &&\
|
||||||
|
apt install cron -y
|
||||||
|
|
||||||
|
COPY --from=build /app/dist/ dist/
|
||||||
|
COPY migrations/ migrations/
|
||||||
|
COPY scripts/startup.sh scripts/
|
||||||
|
|
||||||
|
RUN chmod +x scripts/startup.sh
|
||||||
|
RUN chmod +x -R dist/
|
||||||
|
|
||||||
|
CMD [ "/bin/bash", "scripts/startup.sh" ]
|
5
cmd/api/ascii
Normal file
5
cmd/api/ascii
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
████████ ██████ ███████ ██████ █████
|
||||||
|
██ ██ ██ ██ ██ ██ ██ ██
|
||||||
|
██ ██████ █████ ██████ ███████
|
||||||
|
██ ██ ██ ██ ██ ██ ██
|
||||||
|
██ ██ ██ ███████ ██ ██ ██
|
111
cmd/api/main.go
Normal file
111
cmd/api/main.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/accounts"
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed ascii
|
||||||
|
var ascii string
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println(ascii)
|
||||||
|
|
||||||
|
core.LoadEnvVars()
|
||||||
|
|
||||||
|
db := setupDB()
|
||||||
|
router := setupRouter(db)
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: ":3000",
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdownChan := make(chan os.Signal, 1)
|
||||||
|
done := make(chan bool, 1)
|
||||||
|
signal.Notify(shutdownChan, os.Interrupt, os.Kill, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sig := <-shutdownChan
|
||||||
|
slog.Info("Shutdown signal received", "signal", sig)
|
||||||
|
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
slog.Info("Shutting down HTTP server")
|
||||||
|
|
||||||
|
if err := server.Shutdown(timeoutCtx); err != nil {
|
||||||
|
slog.Error("HTTP server forced to shutdown", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Closing database connection")
|
||||||
|
|
||||||
|
if err := db.Close(); err != nil {
|
||||||
|
slog.Error("Error closing database connection", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Shutting down application")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-timeoutCtx.Done():
|
||||||
|
slog.Error("Shutdown timed out, forcing exit")
|
||||||
|
done <- false
|
||||||
|
default:
|
||||||
|
slog.Info("Graceful shutdown completed")
|
||||||
|
done <- true
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
if err != http.ErrServerClosed {
|
||||||
|
slog.Error("HTTP server failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if success := <-done; !success {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDB() *sqlx.DB {
|
||||||
|
dbURL := core.ComputeDBURL(
|
||||||
|
core.MustGetEnvVar("DB_HOST"),
|
||||||
|
core.MustGetEnvVar("DB_PORT"),
|
||||||
|
core.MustGetEnvVar("DB_USER"),
|
||||||
|
core.MustGetEnvVar("DB_PASSWORD"),
|
||||||
|
core.MustGetEnvVar("DB_NAME"),
|
||||||
|
)
|
||||||
|
|
||||||
|
slog.Info("Connecting to database", "url", dbURL)
|
||||||
|
|
||||||
|
db, err := core.SetupDB(dbURL)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to connect to database", "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Connected to database")
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRouter(db *sqlx.DB) *core.ServerMux {
|
||||||
|
router := core.NewServerMux()
|
||||||
|
|
||||||
|
accounts.BindRoutes(router, db)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
7
cmd/cron/resync.go
Normal file
7
cmd/cron/resync.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Resyncing")
|
||||||
|
}
|
19
go.mod
Normal file
19
go.mod
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
module gitea.qpismont.fr/qpismont/trepa
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jackc/pgx v3.6.2+incompatible
|
||||||
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cockroachdb/apd v1.1.0 // indirect
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||||
|
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
)
|
28
go.sum
Normal file
28
go.sum
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
|
||||||
|
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||||
|
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
|
||||||
|
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
20
internal/accounts/controller.go
Normal file
20
internal/accounts/controller.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
service Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(service Service) Controller {
|
||||||
|
return Controller{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) CreateAccount(w *core.Response, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
w.Json([]byte("test"))
|
||||||
|
}
|
13
internal/accounts/errors.go
Normal file
13
internal/accounts/errors.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAccountNotFound = core.NewHTTPError(http.StatusNotFound, "Account not found", nil)
|
||||||
|
ErrAccountAlreadyExists = core.NewHTTPError(http.StatusConflict, "Account already exists", nil)
|
||||||
|
ErrBadPassword = core.NewHTTPError(http.StatusUnauthorized, "Bad password", nil)
|
||||||
|
)
|
13
internal/accounts/repository.go
Normal file
13
internal/accounts/repository.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(db *sqlx.DB) Repository {
|
||||||
|
return Repository{db: db}
|
||||||
|
}
|
14
internal/accounts/routes.go
Normal file
14
internal/accounts/routes.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.qpismont.fr/qpismont/trepa/internal/core"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BindRoutes(srv *core.ServerMux, db *sqlx.DB) {
|
||||||
|
repository := NewRepository(db)
|
||||||
|
service := NewService(repository)
|
||||||
|
controller := NewController(service)
|
||||||
|
|
||||||
|
srv.HandleFunc("GET /accounts", controller.CreateAccount)
|
||||||
|
}
|
9
internal/accounts/service.go
Normal file
9
internal/accounts/service.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package accounts
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repository Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repository Repository) Service {
|
||||||
|
return Service{repository: repository}
|
||||||
|
}
|
21
internal/core/database.go
Normal file
21
internal/core/database.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
_ "github.com/jackc/pgx/stdlib"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupDB(url string) (*sqlx.DB, error) {
|
||||||
|
db, err := sqlx.Open("pgx", url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ComputeDBURL(dbHost, dbPort, dbUser, dbPassword, dbName string) string {
|
||||||
|
return fmt.Sprintf("postgres://%s:%s@%s:%s/%s", dbUser, dbPassword, dbHost, dbPort, dbName)
|
||||||
|
}
|
38
internal/core/errors.go
Normal file
38
internal/core/errors.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Cause error `json:"-"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HTTPError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HTTPError) Unwrap() error {
|
||||||
|
return e.Cause
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPError(code int, message string, cause error) *HTTPError {
|
||||||
|
details := ""
|
||||||
|
if cause != nil {
|
||||||
|
details = cause.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HTTPError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Cause: cause,
|
||||||
|
Details: details,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInternalServerError(cause error) *HTTPError {
|
||||||
|
return NewHTTPError(http.StatusInternalServerError, "Internal server error", cause)
|
||||||
|
}
|
25
internal/core/helpers.go
Normal file
25
internal/core/helpers.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadEnvVars() {
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Error loading .env file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustGetEnvVar(key string) string {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if value == "" {
|
||||||
|
slog.Error("Environment variable is not set", "key", key)
|
||||||
|
panic("Environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
62
internal/core/http.go
Normal file
62
internal/core/http.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
Status int
|
||||||
|
Size int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponse(w http.ResponseWriter) *Response {
|
||||||
|
return &Response{
|
||||||
|
ResponseWriter: w,
|
||||||
|
Status: 200,
|
||||||
|
Size: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Header() http.Header {
|
||||||
|
return r.ResponseWriter.Header()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) WriteHeader(status int) {
|
||||||
|
r.Status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Json(json []byte) {
|
||||||
|
r.Header().Set("Content-Type", "application/json")
|
||||||
|
r.Write(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Write(b []byte) (int, error) {
|
||||||
|
r.ResponseWriter.WriteHeader(r.Status)
|
||||||
|
n, err := r.ResponseWriter.Write(b)
|
||||||
|
r.Size += n
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerMux struct {
|
||||||
|
mux *http.ServeMux
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServerMux() *ServerMux {
|
||||||
|
return &ServerMux{
|
||||||
|
mux: http.NewServeMux(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerMux) HandleFunc(pattern string, handler func(w *Response, r *http.Request)) {
|
||||||
|
s.mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response := NewResponse(w)
|
||||||
|
|
||||||
|
LogMiddleware(ErrorMiddleware(handler))(response, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s.mux.ServeHTTP(w, r)
|
||||||
|
}
|
46
internal/core/middleware.go
Normal file
46
internal/core/middleware.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LogMiddleware(next func(w *Response, r *http.Request)) func(w *Response, r *http.Request) {
|
||||||
|
return func(w *Response, r *http.Request) {
|
||||||
|
now := time.Now()
|
||||||
|
next(w, r)
|
||||||
|
elapsed := time.Since(now)
|
||||||
|
|
||||||
|
// Use NCSA common log format
|
||||||
|
ip := r.RemoteAddr
|
||||||
|
timestamp := time.Now().Format("02/Jan/2006:15:04:05 -0700")
|
||||||
|
method := r.Method
|
||||||
|
url := r.URL.Path
|
||||||
|
protocol := r.Proto
|
||||||
|
status := w.Status
|
||||||
|
size := w.Size
|
||||||
|
|
||||||
|
slog.Info(fmt.Sprintf("%s - - [%s] \"%s %s %s\" %d %d %s", ip, timestamp, method, url, protocol, status, size, elapsed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorMiddleware(next func(w *Response, r *http.Request)) func(w *Response, r *http.Request) {
|
||||||
|
return func(w *Response, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
if err, ok := err.(error); ok {
|
||||||
|
json, _ := json.Marshal(NewInternalServerError(err))
|
||||||
|
w.Json(json)
|
||||||
|
} else {
|
||||||
|
json, _ := json.Marshal(NewInternalServerError(nil))
|
||||||
|
w.Json(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
14
migrations/20240703103050_accounts.up.sql
Normal file
14
migrations/20240703103050_accounts.up.sql
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS public.accounts
|
||||||
|
(
|
||||||
|
id integer NOT NULL GENERATED ALWAYS AS IDENTITY,
|
||||||
|
username text NOT NULL,
|
||||||
|
password text NOT NULL,
|
||||||
|
role_id smallint NOT NULL DEFAULT '1'::smallint,
|
||||||
|
created_at timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT accounts_pkey PRIMARY KEY (id),
|
||||||
|
CONSTRAINT accounts_username_key UNIQUE (username)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS public.accounts
|
||||||
|
OWNER to dev;
|
16
migrations/20240703103105_movies.up.sql
Normal file
16
migrations/20240703103105_movies.up.sql
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.movies
|
||||||
|
(
|
||||||
|
id integer NOT NULL GENERATED ALWAYS AS IDENTITY,
|
||||||
|
title text NOT NULL,
|
||||||
|
overview text NOT NULL,
|
||||||
|
poster_path text NOT NULL,
|
||||||
|
backdrop_path text,
|
||||||
|
release_date date NOT NULL,
|
||||||
|
tmdb_id integer NOT NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS public.movies
|
||||||
|
OWNER to dev;
|
12
scripts/build.sh
Normal file
12
scripts/build.sh
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#Optimize build size
|
||||||
|
export CGO_ENABLED=0
|
||||||
|
export GOOS=linux
|
||||||
|
export GOARCH=amd64
|
||||||
|
|
||||||
|
#Build the API
|
||||||
|
go build -ldflags="-s -w" -o dist/api ./cmd/api/main.go
|
||||||
|
|
||||||
|
#Build the cron job
|
||||||
|
go build -ldflags="-s -w" -o dist/resync ./cmd/cron/resync.go
|
10
scripts/startup.sh
Normal file
10
scripts/startup.sh
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#Start cron service (Debian)
|
||||||
|
service cron start
|
||||||
|
|
||||||
|
# Wait for the database to be ready
|
||||||
|
./migrate -source file://migrations/ -database "$DATABASE_URL" up
|
||||||
|
|
||||||
|
# Run the API
|
||||||
|
./dist/api
|
Loading…
Reference in a new issue