refactor: adopt portigo infrastructure patterns
Add config package with build-tag-switched dev/prod environments, structured logging via zerolog, Taskfile for dev workflow, golangci-lint config, testutil package, and improved DB setup with proper SQLite pragmas and cleanup. Rename sqlc output package from gen to repository. Switch to allowlist .gitignore, Alpine+UPX+scratch Dockerfile, and CI pipeline with test/lint gates before deploy.
This commit is contained in:
15
.env.example
15
.env.example
@@ -1,5 +1,16 @@
|
||||
# Application URL for invite links (defaults to https://games.adriatica.io)
|
||||
# Log level (TRACE, DEBUG, INFO, WARN, ERROR). Defaults to INFO.
|
||||
# LOG_LEVEL=DEBUG
|
||||
|
||||
# SQLite database path. Defaults to data/c4.db.
|
||||
# DB_PATH=data/c4.db
|
||||
|
||||
# Application URL for invite links. Defaults to https://games.adriatica.io.
|
||||
# APP_URL=http://localhost:7331
|
||||
|
||||
# Server port (defaults to 7331)
|
||||
# Server port. Defaults to 7331.
|
||||
# PORT=7331
|
||||
|
||||
# Goose CLI migration config (only needed for running goose manually)
|
||||
GOOSE_DRIVER=sqlite3
|
||||
GOOSE_DBSTRING=data/c4.db?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)
|
||||
GOOSE_MIGRATION_DIR=db/migrations
|
||||
|
||||
@@ -1,14 +1,44 @@
|
||||
name: Deploy c4
|
||||
name: CI / Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
DEPLOY_DIR: /home/ryan/c4
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- name: Run tests
|
||||
run: go test ./...
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
|
||||
- name: Install golangci-lint
|
||||
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
|
||||
- name: Run linter
|
||||
run: golangci-lint run
|
||||
|
||||
deploy:
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
needs: [test, lint]
|
||||
runs-on: games
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -21,8 +51,6 @@ jobs:
|
||||
- name: Ensure data directory exists with correct ownership
|
||||
run: |
|
||||
mkdir -p $DEPLOY_DIR/data
|
||||
# UID 5 / GID 60 = games:games in the container (debian:bookworm-slim)
|
||||
sudo chown 5:60 $DEPLOY_DIR/data
|
||||
|
||||
- name: Rebuild and restart
|
||||
run: cd $DEPLOY_DIR && docker compose up -d --build --remove-orphans
|
||||
|
||||
40
.gitignore
vendored
40
.gitignore
vendored
@@ -1,8 +1,34 @@
|
||||
c4
|
||||
c4.db
|
||||
data/
|
||||
.env
|
||||
# Allowlisting gitignore: ignore everything, then un-ignore what we track.
|
||||
# source: https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
|
||||
# Deploy artifacts
|
||||
c4-deploy-*.tar.gz
|
||||
c4-deploy-*_b64*.txt
|
||||
# Ignore everything
|
||||
*
|
||||
|
||||
# But not these files...
|
||||
!.gitignore
|
||||
|
||||
!*.go
|
||||
!*.sql
|
||||
!go.sum
|
||||
!go.mod
|
||||
!Taskfile.yml
|
||||
!sqlc.yaml
|
||||
!.golangci.yml
|
||||
!.gitea/workflows/*.yml
|
||||
|
||||
!.env.example
|
||||
!LICENSE
|
||||
|
||||
!assets/**/*
|
||||
|
||||
# Generated CSS stays out of version control
|
||||
assets/css/output.css
|
||||
|
||||
# Deploy scripts and configs
|
||||
!deploy/*.sh
|
||||
!deploy/*.service
|
||||
!docker-compose.yml
|
||||
!Dockerfile
|
||||
|
||||
# ...even if they are in subdirectories
|
||||
!*/
|
||||
|
||||
45
.golangci.yml
Normal file
45
.golangci.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
default: standard
|
||||
enable:
|
||||
- errcheck
|
||||
- govet
|
||||
- staticcheck
|
||||
- gosec
|
||||
- bodyclose
|
||||
- sqlclosecheck
|
||||
- misspell
|
||||
- errname
|
||||
- copyloopvar
|
||||
|
||||
settings:
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- "-ST1001" # dot imports
|
||||
- "-ST1003" # naming conventions
|
||||
gosec:
|
||||
excludes:
|
||||
- G104 # unhandled errors — redundant with errcheck
|
||||
- G107 # HTTP requests with variable URLs — expected in a web app
|
||||
- G115 # integer overflow conversion
|
||||
- G301 # directory permissions 0750 — 0755 is standard for data dirs
|
||||
- G404 # weak random — acceptable for game IDs and player IDs
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
|
||||
settings:
|
||||
goimports:
|
||||
local-prefixes:
|
||||
- github.com/ryanhamamura/c4
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gosec
|
||||
- errcheck
|
||||
31
Dockerfile
31
Dockerfile
@@ -1,4 +1,6 @@
|
||||
FROM golang:1.25.4-bookworm AS build
|
||||
FROM docker.io/golang:1.25.4-alpine AS build
|
||||
|
||||
RUN apk add --no-cache upx
|
||||
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
@@ -6,24 +8,11 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN go tool gotailwind -i assets/css/input.css -o assets/css/output.css --minify
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /c4 .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 go build -ldflags="-s" -o /bin/c4 .
|
||||
RUN upx -9 -k /bin/c4
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates wget && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /c4 /usr/local/bin/c4
|
||||
|
||||
WORKDIR /app
|
||||
RUN mkdir data && chown games:games data
|
||||
|
||||
USER games
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD wget -qO /dev/null http://localhost:8080/
|
||||
|
||||
CMD ["c4"]
|
||||
FROM scratch
|
||||
ENV PORT=8080
|
||||
COPY --from=build /bin/c4 /
|
||||
ENTRYPOINT ["/c4"]
|
||||
|
||||
63
Taskfile.yml
Normal file
63
Taskfile.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
version: "3"
|
||||
|
||||
tasks:
|
||||
build:styles:
|
||||
desc: Build TailwindCSS styles
|
||||
cmds:
|
||||
- go tool gotailwind -i assets/css/input.css -o assets/css/output.css --minify
|
||||
sources:
|
||||
- "assets/css/input.css"
|
||||
- "**/*.go"
|
||||
generates:
|
||||
- "assets/css/output.css"
|
||||
|
||||
build:
|
||||
desc: Production build to bin/c4
|
||||
cmds:
|
||||
- go build -o bin/c4 .
|
||||
deps:
|
||||
- build:styles
|
||||
|
||||
live:styles:
|
||||
desc: Watch and rebuild TailwindCSS styles
|
||||
cmds:
|
||||
- go tool gotailwind -i assets/css/input.css -o assets/css/output.css -w
|
||||
|
||||
live:server:
|
||||
desc: Run server with hot-reload via air
|
||||
cmds:
|
||||
- |
|
||||
go tool air \
|
||||
-build.cmd "go build -tags=dev -o tmp/bin/c4 ." \
|
||||
-build.bin "tmp/bin/c4" \
|
||||
-build.exclude_dir "data,bin,tmp,deploy" \
|
||||
-build.include_ext "go" \
|
||||
-misc.clean_on_exit "true"
|
||||
|
||||
live:
|
||||
desc: Dev mode with hot-reload
|
||||
deps:
|
||||
- live:styles
|
||||
- live:server
|
||||
|
||||
test:
|
||||
desc: Run the test suite
|
||||
cmds:
|
||||
- go test ./...
|
||||
|
||||
lint:
|
||||
desc: Run golangci-lint
|
||||
cmds:
|
||||
- golangci-lint run
|
||||
|
||||
run:
|
||||
desc: Build and run the server
|
||||
cmds:
|
||||
- ./bin/c4
|
||||
deps:
|
||||
- build
|
||||
|
||||
default:
|
||||
desc: Run the default task (live)
|
||||
cmds:
|
||||
- task: live
|
||||
73
config/config.go
Normal file
73
config/config.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Package config provides build-tag-switched application configuration.
|
||||
// The global Config singleton is initialized at import time via init()
|
||||
// and can be overridden in tests with LoadForTest().
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Environment string
|
||||
|
||||
const (
|
||||
Dev Environment = "dev"
|
||||
Prod Environment = "prod"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Environment Environment
|
||||
Host string
|
||||
Port string
|
||||
LogLevel zerolog.Level
|
||||
AppURL string
|
||||
DBPath string
|
||||
}
|
||||
|
||||
var (
|
||||
Global *Config
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func init() {
|
||||
once.Do(func() {
|
||||
Global = Load()
|
||||
})
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if val, ok := os.LookupEnv(key); ok {
|
||||
return val
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func loadBase() *Config {
|
||||
godotenv.Load() //nolint:errcheck // .env file is optional
|
||||
|
||||
return &Config{
|
||||
Host: getEnv("HOST", "0.0.0.0"),
|
||||
Port: getEnv("PORT", "7331"),
|
||||
LogLevel: func() zerolog.Level {
|
||||
switch os.Getenv("LOG_LEVEL") {
|
||||
case "TRACE":
|
||||
return zerolog.TraceLevel
|
||||
case "DEBUG":
|
||||
return zerolog.DebugLevel
|
||||
case "INFO":
|
||||
return zerolog.InfoLevel
|
||||
case "WARN":
|
||||
return zerolog.WarnLevel
|
||||
case "ERROR":
|
||||
return zerolog.ErrorLevel
|
||||
default:
|
||||
return zerolog.InfoLevel
|
||||
}
|
||||
}(),
|
||||
AppURL: getEnv("APP_URL", "https://games.adriatica.io"),
|
||||
DBPath: getEnv("DB_PATH", "data/c4.db"),
|
||||
}
|
||||
}
|
||||
9
config/config_dev.go
Normal file
9
config/config_dev.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build dev
|
||||
|
||||
package config
|
||||
|
||||
func Load() *Config {
|
||||
cfg := loadBase()
|
||||
cfg.Environment = Dev
|
||||
return cfg
|
||||
}
|
||||
9
config/config_prod.go
Normal file
9
config/config_prod.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !dev
|
||||
|
||||
package config
|
||||
|
||||
func Load() *Config {
|
||||
cfg := loadBase()
|
||||
cfg.Environment = Prod
|
||||
return cfg
|
||||
}
|
||||
19
config/config_test_helper.go
Normal file
19
config/config_test_helper.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// LoadForTest sets config.Global to safe defaults without reading
|
||||
// environment variables or .env files. Call this in TestMain or at the
|
||||
// top of tests that import packages which depend on config.Global.
|
||||
func LoadForTest() {
|
||||
Global = &Config{
|
||||
Environment: Dev,
|
||||
Host: "127.0.0.1",
|
||||
Port: "0",
|
||||
LogLevel: zerolog.WarnLevel,
|
||||
AppURL: "http://localhost:0",
|
||||
DBPath: ":memory:",
|
||||
}
|
||||
}
|
||||
57
db/db.go
57
db/db.go
@@ -1,33 +1,70 @@
|
||||
// Package db handles SQLite database setup, pragma configuration, and
|
||||
// goose migrations.
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrations embed.FS
|
||||
var MigrationFS embed.FS
|
||||
|
||||
var DB *sql.DB
|
||||
|
||||
func Init(dbPath string) error {
|
||||
func Init(dbPath string) (func(), error) {
|
||||
if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
|
||||
return nil, fmt.Errorf("creating data dir: %w", err)
|
||||
}
|
||||
|
||||
// busy_timeout must be first because the connection needs to block on
|
||||
// busy before WAL mode is set in case it hasn't been set already.
|
||||
pragmas := "?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=journal_size_limit(200000000)&_pragma=synchronous(NORMAL)&_pragma=foreign_keys(ON)&_pragma=temp_store(MEMORY)&_pragma=cache_size(-32000)"
|
||||
var err error
|
||||
DB, err = sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
|
||||
DB, err = goose.OpenDBWithDriver("sqlite", dbPath+pragmas)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, fmt.Errorf("opening database: %w", err)
|
||||
}
|
||||
DB.SetMaxOpenConns(1)
|
||||
|
||||
goose.SetBaseFS(migrations)
|
||||
if err := DB.Ping(); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("pinging database: %w", err), DB.Close())
|
||||
}
|
||||
slog.Info("db connected", "db", dbPath)
|
||||
|
||||
sub, err := fs.Sub(MigrationFS, "migrations")
|
||||
if err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("migrations sub fs: %w", err), DB.Close())
|
||||
}
|
||||
goose.SetBaseFS(sub)
|
||||
|
||||
if err := goose.SetDialect("sqlite3"); err != nil {
|
||||
return err
|
||||
return nil, errors.Join(fmt.Errorf("setting goose dialect: %w", err), DB.Close())
|
||||
}
|
||||
if err := goose.Up(DB, "migrations"); err != nil {
|
||||
return err
|
||||
if err := goose.Up(DB, "."); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("running migrations: %w", err), DB.Close())
|
||||
}
|
||||
|
||||
return nil
|
||||
if _, err := DB.Exec("PRAGMA optimize"); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("pragma optimize: %w", err), DB.Close())
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
if _, err := DB.Exec("PRAGMA optimize(0x10002)"); err != nil {
|
||||
slog.Error("pragma optimize at shutdown", "error", err)
|
||||
}
|
||||
if err := DB.Close(); err != nil {
|
||||
slog.Error("closing database", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return cleanup, nil
|
||||
}
|
||||
|
||||
@@ -5,17 +5,17 @@ import (
|
||||
"database/sql"
|
||||
"slices"
|
||||
|
||||
"github.com/ryanhamamura/c4/db/gen"
|
||||
"github.com/ryanhamamura/c4/db/repository"
|
||||
"github.com/ryanhamamura/c4/game"
|
||||
"github.com/ryanhamamura/c4/snake"
|
||||
"github.com/ryanhamamura/c4/ui"
|
||||
)
|
||||
|
||||
type GamePersister struct {
|
||||
queries *gen.Queries
|
||||
queries *repository.Queries
|
||||
}
|
||||
|
||||
func NewGamePersister(q *gen.Queries) *GamePersister {
|
||||
func NewGamePersister(q *repository.Queries) *GamePersister {
|
||||
return &GamePersister{queries: q}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (p *GamePersister) SaveGame(g *game.Game) error {
|
||||
|
||||
_, err := p.queries.GetGame(ctx, g.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = p.queries.CreateGame(ctx, gen.CreateGameParams{
|
||||
_, err = p.queries.CreateGame(ctx, repository.CreateGameParams{
|
||||
ID: g.ID,
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
@@ -51,7 +51,7 @@ func (p *GamePersister) SaveGame(g *game.Game) error {
|
||||
rematchGameID = sql.NullString{String: *g.RematchGameID, Valid: true}
|
||||
}
|
||||
|
||||
return p.queries.UpdateGame(ctx, gen.UpdateGameParams{
|
||||
return p.queries.UpdateGame(ctx, repository.UpdateGameParams{
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
Status: int64(g.Status),
|
||||
@@ -100,7 +100,7 @@ func (p *GamePersister) SaveGamePlayer(gameID string, player *game.Player, slot
|
||||
guestPlayerID = sql.NullString{String: string(player.ID), Valid: true}
|
||||
}
|
||||
|
||||
return p.queries.CreateGamePlayer(ctx, gen.CreateGamePlayerParams{
|
||||
return p.queries.CreateGamePlayer(ctx, repository.CreateGamePlayerParams{
|
||||
GameID: gameID,
|
||||
UserID: userID,
|
||||
GuestPlayerID: guestPlayerID,
|
||||
@@ -144,10 +144,10 @@ func (p *GamePersister) DeleteGame(id string) error {
|
||||
|
||||
// SnakePersister implements snake.Persister
|
||||
type SnakePersister struct {
|
||||
queries *gen.Queries
|
||||
queries *repository.Queries
|
||||
}
|
||||
|
||||
func NewSnakePersister(q *gen.Queries) *SnakePersister {
|
||||
func NewSnakePersister(q *repository.Queries) *SnakePersister {
|
||||
return &SnakePersister{queries: q}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (p *SnakePersister) SaveSnakeGame(sg *snake.SnakeGame) error {
|
||||
|
||||
_, err := p.queries.GetSnakeGame(ctx, sg.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = p.queries.CreateSnakeGame(ctx, gen.CreateSnakeGameParams{
|
||||
_, err = p.queries.CreateSnakeGame(ctx, repository.CreateSnakeGameParams{
|
||||
ID: sg.ID,
|
||||
Board: boardJSON,
|
||||
Status: int64(sg.Status),
|
||||
@@ -192,7 +192,7 @@ func (p *SnakePersister) SaveSnakeGame(sg *snake.SnakeGame) error {
|
||||
rematchGameID = sql.NullString{String: *sg.RematchGameID, Valid: true}
|
||||
}
|
||||
|
||||
return p.queries.UpdateSnakeGame(ctx, gen.UpdateSnakeGameParams{
|
||||
return p.queries.UpdateSnakeGame(ctx, repository.UpdateSnakeGameParams{
|
||||
Board: boardJSON,
|
||||
Status: int64(sg.Status),
|
||||
WinnerUserID: winnerUserID,
|
||||
@@ -247,7 +247,7 @@ func (p *SnakePersister) SaveSnakePlayer(gameID string, player *snake.Player) er
|
||||
guestPlayerID = sql.NullString{String: string(player.ID), Valid: true}
|
||||
}
|
||||
|
||||
return p.queries.CreateSnakePlayer(ctx, gen.CreateSnakePlayerParams{
|
||||
return p.queries.CreateSnakePlayer(ctx, repository.CreateSnakePlayerParams{
|
||||
GameID: gameID,
|
||||
UserID: userID,
|
||||
GuestPlayerID: guestPlayerID,
|
||||
@@ -290,15 +290,15 @@ func (p *SnakePersister) DeleteSnakeGame(id string) error {
|
||||
}
|
||||
|
||||
type ChatPersister struct {
|
||||
queries *gen.Queries
|
||||
queries *repository.Queries
|
||||
}
|
||||
|
||||
func NewChatPersister(q *gen.Queries) *ChatPersister {
|
||||
func NewChatPersister(q *repository.Queries) *ChatPersister {
|
||||
return &ChatPersister{queries: q}
|
||||
}
|
||||
|
||||
func (p *ChatPersister) SaveChatMessage(gameID string, msg ui.C4ChatMessage) error {
|
||||
return p.queries.CreateChatMessage(context.Background(), gen.CreateChatMessageParams{
|
||||
return p.queries.CreateChatMessage(context.Background(), repository.CreateChatMessageParams{
|
||||
GameID: gameID,
|
||||
Nickname: msg.Nickname,
|
||||
Color: int64(msg.Color),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// sqlc v1.30.0
|
||||
// source: chat.sql
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -2,7 +2,7 @@
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -3,7 +3,7 @@
|
||||
// sqlc v1.30.0
|
||||
// source: games.sql
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -2,7 +2,7 @@
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
@@ -3,7 +3,7 @@
|
||||
// sqlc v1.30.0
|
||||
// source: snake_games.sql
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -3,7 +3,7 @@
|
||||
// sqlc v1.30.0
|
||||
// source: users.sql
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -5,5 +5,9 @@ sql:
|
||||
schema: "migrations"
|
||||
gen:
|
||||
go:
|
||||
package: "gen"
|
||||
out: "gen"
|
||||
package: "repository"
|
||||
out: "repository"
|
||||
emit_db_tags: true
|
||||
emit_json_tags: true
|
||||
emit_result_struct_pointers: true
|
||||
emit_pointers_for_null_types: true
|
||||
|
||||
27
go.mod
27
go.mod
@@ -3,27 +3,28 @@ module github.com/ryanhamamura/c4
|
||||
go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de
|
||||
github.com/alexedwards/scs/v2 v2.9.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/pressly/goose/v3 v3.26.0
|
||||
github.com/pressly/goose/v3 v3.27.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/ryanhamamura/via v0.23.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
modernc.org/sqlite v1.44.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
modernc.org/sqlite v1.46.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/CAFxX/httpcompression v0.0.9 // indirect
|
||||
github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de // indirect
|
||||
github.com/alexedwards/scs/v2 v2.9.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/antithesishq/antithesis-sdk-go v0.5.0 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/delaneyj/toolbelt v0.9.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/go-tpm v0.9.7 // indirect
|
||||
github.com/hookenz/gotailwind/v4 v4.1.18 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/hookenz/gotailwind/v4 v4.2.1 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
|
||||
@@ -33,18 +34,18 @@ require (
|
||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/starfederation/datastar-go v1.0.3 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
maragu.dev/gomponents v1.2.0 // indirect
|
||||
modernc.org/libc v1.67.4 // indirect
|
||||
modernc.org/libc v1.68.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
62
go.sum
62
go.sum
@@ -31,16 +31,17 @@ 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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hookenz/gotailwind/v4 v4.1.18 h1:1h3XwTVx1dEBm6A0bcosAplNCde+DCmVJG0arLy5fBE=
|
||||
github.com/hookenz/gotailwind/v4 v4.1.18/go.mod h1:IfiJtdp8ExV9HV2XUiVjRBvB3QewVXVKWoAGEcpjfNE=
|
||||
github.com/hookenz/gotailwind/v4 v4.2.1 h1:FpZLtAAbHH7wMvyGYT+01vTLFITGMGZGMtEbp7dd2dM=
|
||||
github.com/hookenz/gotailwind/v4 v4.2.1/go.mod h1:IfiJtdp8ExV9HV2XUiVjRBvB3QewVXVKWoAGEcpjfNE=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -65,21 +66,20 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
|
||||
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
||||
github.com/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM=
|
||||
github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78=
|
||||
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/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/ryanhamamura/via v0.21.2 h1:osR6peY/mZSl9SNPeEv6IvzGU0akkQfQzQJgA74+7mk=
|
||||
github.com/ryanhamamura/via v0.21.2/go.mod h1:rpJewNVG6tgginZN7Be3qqRuol70+v1sFCKD4UjHsQo=
|
||||
github.com/ryanhamamura/via v0.23.0 h1:0e7nytisazcWq7uxs6T27GM3FwzosCMenkxJd+78Lko=
|
||||
github.com/ryanhamamura/via v0.23.0/go.mod h1:rpJewNVG6tgginZN7Be3qqRuol70+v1sFCKD4UjHsQo=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
@@ -103,24 +103,24 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
@@ -129,18 +129,18 @@ maragu.dev/gomponents v1.2.0 h1:H7/N5htz1GCnhu0HB1GasluWeU2rJZOYztVEyN61iTc=
|
||||
maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
|
||||
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
|
||||
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/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
|
||||
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
||||
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
|
||||
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
|
||||
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=
|
||||
@@ -149,8 +149,8 @@ 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.44.0 h1:YjCKJnzZde2mLVy0cMKTSL4PxCmbIguOq9lGp8ZvGOc=
|
||||
modernc.org/sqlite v1.44.0/go.mod h1:2Dq41ir5/qri7QJJJKNZcP4UF7TsX/KNeykYgPDtGhE=
|
||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
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=
|
||||
|
||||
41
logging/log.go
Normal file
41
logging/log.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Package logging configures zerolog and provides HTTP request logging middleware.
|
||||
package logging
|
||||
|
||||
import (
|
||||
"io"
|
||||
stdlog "log"
|
||||
"os"
|
||||
|
||||
"github.com/ryanhamamura/c4/config"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
)
|
||||
|
||||
func SetupLogger(env config.Environment, level zerolog.Level) *zerolog.Logger {
|
||||
zerolog.ErrorStackFieldName = "stack_trace"
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
|
||||
zerolog.SetGlobalLevel(level)
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
var output io.Writer
|
||||
switch env {
|
||||
case config.Dev:
|
||||
output = zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: "2006/01/02 15:04:05",
|
||||
}
|
||||
case config.Prod:
|
||||
output = os.Stderr
|
||||
}
|
||||
|
||||
logger := zerolog.New(output).With().Timestamp().Stack().Logger()
|
||||
zerolog.DefaultContextLogger = &logger
|
||||
log.Logger = logger
|
||||
|
||||
stdlog.SetFlags(0)
|
||||
stdlog.SetOutput(logger)
|
||||
return &logger
|
||||
}
|
||||
126
logging/middleware.go
Normal file
126
logging/middleware.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ryanhamamura/c4/config"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
ansiReset = "\033[0m"
|
||||
ansiBrightRed = "\033[31;1m"
|
||||
ansiBrightGreen = "\033[32;1m"
|
||||
ansiBrightYellow = "\033[33;1m"
|
||||
ansiBrightMagenta = "\033[35;1m"
|
||||
ansiBrightCyan = "\033[36;1m"
|
||||
ansiGreen = "\033[32m"
|
||||
ansiYellow = "\033[33m"
|
||||
ansiRed = "\033[31m"
|
||||
)
|
||||
|
||||
func colorStatus(status int, useColor bool) string {
|
||||
s := fmt.Sprintf("%d", status)
|
||||
if !useColor {
|
||||
return s
|
||||
}
|
||||
switch {
|
||||
case status < 200:
|
||||
return ansiBrightGreen + s + ansiReset
|
||||
case status < 300:
|
||||
return ansiBrightGreen + s + ansiReset
|
||||
case status < 400:
|
||||
return ansiBrightCyan + s + ansiReset
|
||||
case status < 500:
|
||||
return ansiBrightYellow + s + ansiReset
|
||||
default:
|
||||
return ansiBrightRed + s + ansiReset
|
||||
}
|
||||
}
|
||||
|
||||
func colorMethod(method string, useColor bool) string {
|
||||
if !useColor {
|
||||
return method
|
||||
}
|
||||
return ansiBrightMagenta + method + ansiReset
|
||||
}
|
||||
|
||||
func colorLatency(d time.Duration, useColor bool) string {
|
||||
s := d.String()
|
||||
if !useColor {
|
||||
return s
|
||||
}
|
||||
switch {
|
||||
case d < 500*time.Millisecond:
|
||||
return ansiGreen + s + ansiReset
|
||||
case d < 5*time.Second:
|
||||
return ansiYellow + s + ansiReset
|
||||
default:
|
||||
return ansiRed + s + ansiReset
|
||||
}
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.status = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func RequestLogger(logger *zerolog.Logger, env config.Environment) 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()
|
||||
rw := &responseWriter{ResponseWriter: w}
|
||||
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
status := rw.status
|
||||
if status == 0 {
|
||||
status = http.StatusOK
|
||||
}
|
||||
l := log.Ctx(r.Context())
|
||||
if l.GetLevel() == zerolog.Disabled {
|
||||
l = logger
|
||||
}
|
||||
|
||||
var evt *zerolog.Event
|
||||
switch {
|
||||
case status < 400:
|
||||
evt = l.Info()
|
||||
case status < 500:
|
||||
evt = l.Warn()
|
||||
case status < 600:
|
||||
evt = l.Error()
|
||||
default:
|
||||
evt = l.Info()
|
||||
}
|
||||
|
||||
latency := time.Since(start)
|
||||
switch env {
|
||||
case config.Dev:
|
||||
useColor := true
|
||||
evt.Msg(fmt.Sprintf("%s %s %s [%s]",
|
||||
colorStatus(status, useColor),
|
||||
colorMethod(r.Method, useColor),
|
||||
r.URL.Path,
|
||||
colorLatency(latency, useColor),
|
||||
))
|
||||
default:
|
||||
evt.
|
||||
Int("status", status).
|
||||
Str("method", r.Method).
|
||||
Str("path", r.URL.Path).
|
||||
Dur("latency", latency).
|
||||
Msg("request")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
44
main.go
44
main.go
@@ -8,20 +8,20 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
"github.com/ryanhamamura/c4/auth"
|
||||
"github.com/ryanhamamura/c4/config"
|
||||
"github.com/ryanhamamura/c4/db"
|
||||
"github.com/ryanhamamura/c4/db/gen"
|
||||
"github.com/ryanhamamura/c4/db/repository"
|
||||
"github.com/ryanhamamura/c4/game"
|
||||
"github.com/ryanhamamura/c4/logging"
|
||||
"github.com/ryanhamamura/c4/snake"
|
||||
"github.com/ryanhamamura/c4/ui"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/ryanhamamura/via"
|
||||
"github.com/ryanhamamura/via/h"
|
||||
)
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
var (
|
||||
store = game.NewGameStore()
|
||||
snakeStore = snake.NewSnakeStore()
|
||||
queries *gen.Queries
|
||||
queries *repository.Queries
|
||||
chatPersister *db.ChatPersister
|
||||
)
|
||||
|
||||
@@ -43,37 +43,33 @@ func DaisyUIPlugin(v *via.V) {
|
||||
v.AppendToHead(h.Link(h.Rel("stylesheet"), h.Href("/assets/css/output.css?v="+version)))
|
||||
}
|
||||
|
||||
func port() string {
|
||||
if p := os.Getenv("PORT"); p != "" {
|
||||
return p
|
||||
}
|
||||
return "7331"
|
||||
}
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load()
|
||||
cfg := config.Global
|
||||
logger := logging.SetupLogger(cfg.Environment, cfg.LogLevel)
|
||||
|
||||
if err := os.MkdirAll("data", 0o755); err != nil {
|
||||
log.Fatal(err)
|
||||
cleanupDB, err := db.Init(cfg.DBPath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("initializing database")
|
||||
}
|
||||
if err := db.Init("data/c4.db"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
queries = gen.New(db.DB)
|
||||
defer cleanupDB()
|
||||
|
||||
queries = repository.New(db.DB)
|
||||
store.SetPersister(db.NewGamePersister(queries))
|
||||
snakeStore.SetPersister(db.NewSnakePersister(queries))
|
||||
chatPersister = db.NewChatPersister(queries)
|
||||
|
||||
sessionManager, err := via.NewSQLiteSessionManager(db.DB)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatal().Err(err).Msg("creating session manager")
|
||||
}
|
||||
|
||||
_ = logger
|
||||
|
||||
v := via.New()
|
||||
v.Config(via.Options{
|
||||
LogLevel: via.LogLevelDebug,
|
||||
DocumentTitle: "Game Lobby",
|
||||
ServerAddress: ":" + port(),
|
||||
ServerAddress: ":" + cfg.Port,
|
||||
SessionManager: sessionManager,
|
||||
Plugins: []via.Plugin{DaisyUIPlugin},
|
||||
})
|
||||
@@ -315,7 +311,7 @@ func main() {
|
||||
|
||||
ctx := context.Background()
|
||||
id := uuid.New().String()
|
||||
user, err := queries.CreateUser(ctx, gen.CreateUserParams{
|
||||
user, err := queries.CreateUser(ctx, repository.CreateUserParams{
|
||||
ID: id,
|
||||
Username: username.String(),
|
||||
PasswordHash: hash,
|
||||
|
||||
47
testutil/db.go
Normal file
47
testutil/db.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Package testutil provides composable test helpers for spinning up
|
||||
// real infrastructure (in-memory SQLite, session managers) in
|
||||
// integration tests.
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"io/fs"
|
||||
"testing"
|
||||
|
||||
"github.com/ryanhamamura/c4/db"
|
||||
"github.com/ryanhamamura/c4/db/repository"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// NewTestDB opens an in-memory SQLite database with the same pragmas as
|
||||
// production, runs all goose migrations, and returns the raw connection
|
||||
// alongside the sqlc Queries handle. The database is closed automatically
|
||||
// when the test finishes.
|
||||
func NewTestDB(t *testing.T) (*sql.DB, *repository.Queries) {
|
||||
t.Helper()
|
||||
|
||||
pragmas := "?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=journal_size_limit(200000000)&_pragma=synchronous(NORMAL)&_pragma=foreign_keys(ON)&_pragma=temp_store(MEMORY)&_pragma=cache_size(-32000)"
|
||||
database, err := goose.OpenDBWithDriver("sqlite", ":memory:"+pragmas)
|
||||
if err != nil {
|
||||
t.Fatalf("open test database: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { database.Close() }) //nolint:errcheck // test cleanup
|
||||
|
||||
if err := database.Ping(); err != nil {
|
||||
t.Fatalf("ping test database: %v", err)
|
||||
}
|
||||
|
||||
sub, err := fs.Sub(db.MigrationFS, "migrations")
|
||||
if err != nil {
|
||||
t.Fatalf("migrations sub fs: %v", err)
|
||||
}
|
||||
goose.SetBaseFS(sub)
|
||||
|
||||
if err := goose.Up(database, "."); err != nil {
|
||||
t.Fatalf("run migrations: %v", err)
|
||||
}
|
||||
|
||||
return database, repository.New(database)
|
||||
}
|
||||
31
testutil/sessions.go
Normal file
31
testutil/sessions.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/sqlite3store"
|
||||
"github.com/alexedwards/scs/v2"
|
||||
)
|
||||
|
||||
// NewTestSessionManager creates an SCS session manager backed by the
|
||||
// provided SQLite database. The background cleanup goroutine is stopped
|
||||
// automatically when the test finishes.
|
||||
func NewTestSessionManager(t *testing.T, db *sql.DB) *scs.SessionManager {
|
||||
t.Helper()
|
||||
|
||||
store := sqlite3store.New(db)
|
||||
t.Cleanup(func() { store.StopCleanup() })
|
||||
|
||||
sm := scs.New()
|
||||
sm.Store = store
|
||||
sm.Lifetime = 30 * 24 * time.Hour
|
||||
sm.Cookie.Path = "/"
|
||||
sm.Cookie.HttpOnly = true
|
||||
sm.Cookie.Secure = false
|
||||
sm.Cookie.SameSite = http.SameSiteLaxMode
|
||||
|
||||
return sm
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ryanhamamura/c4/config"
|
||||
"github.com/ryanhamamura/c4/game"
|
||||
"github.com/ryanhamamura/via/h"
|
||||
)
|
||||
@@ -118,10 +117,7 @@ func PlayerInfo(g *game.Game, myColor int) h.H {
|
||||
}
|
||||
|
||||
func getBaseURL() string {
|
||||
if url := os.Getenv("APP_URL"); url != "" {
|
||||
return url
|
||||
}
|
||||
return "https://games.adriatica.io"
|
||||
return config.Global.AppURL
|
||||
}
|
||||
|
||||
func InviteLink(gameID string) h.H {
|
||||
|
||||
Reference in New Issue
Block a user