added workflows and fixed router
This commit is contained in:
51
.gitea/workflows/build.yml
Normal file
51
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Build and Push Docker Image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and push image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-docker-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-docker-
|
||||||
|
|
||||||
|
- name: Login to Docker Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: registry.syntinel.dev
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
registry.syntinel.dev/junk2jive-server:latest
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
||||||
26
.gitea/workflows/linter.yml
Normal file
26
.gitea/workflows/linter.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: golangci-lint
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
golangci:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v7
|
||||||
|
with:
|
||||||
|
version: v2.0
|
||||||
32
.gitea/workflows/test.yaml
Normal file
32
.gitea/workflows/test.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Run Go Tests
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
|
||||||
|
- name: Cache Go Modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build ./cmd/junk2jive
|
||||||
|
|
||||||
|
- name: Go Test
|
||||||
|
run: NONLOCAL_TESTS=1 go test ./... -v
|
||||||
|
|
||||||
|
- name: Coverage Test
|
||||||
|
run:
|
||||||
|
|
||||||
@@ -9,32 +9,10 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config"
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config"
|
||||||
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers"
|
|
||||||
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes"
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Parse command line flags
|
|
||||||
var configPath string
|
|
||||||
flag.StringVar(&configPath, "config", "", "Path to config file")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
// If no config file specified, look for it in default locations
|
|
||||||
if configPath == "" {
|
|
||||||
// Check current directory
|
|
||||||
if _, err := os.Stat("config.json"); err == nil {
|
|
||||||
configPath = "config.json"
|
|
||||||
} else {
|
|
||||||
// Check config directory relative to executable
|
|
||||||
exePath, err := os.Executable()
|
|
||||||
if err == nil {
|
|
||||||
potentialPath := filepath.Join(filepath.Dir(exePath), "../config/config.json")
|
|
||||||
if _, err := os.Stat(potentialPath); err == nil {
|
|
||||||
configPath = potentialPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
cfg, err := config.Load(configPath)
|
cfg, err := config.Load(configPath)
|
||||||
|
|||||||
99
coverage.out
Normal file
99
coverage.out
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
mode: set
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:13.27,19.2 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:21.57,22.45 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:22.45,24.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:25.5,25.24 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/text.go:20.61,21.57 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/text.go:21.57,23.68 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/text.go:23.68,26.10 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/text.go:28.9,30.23 3 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/text.go:30.23,33.10 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/text.go:35.9,41.44 3 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:16.59,17.57 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:17.57,19.62 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:19.62,22.10 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:24.9,25.23 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:25.23,28.10 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:29.9,33.23 3 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:33.23,36.10 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:37.9,41.57 3 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:41.57,44.10 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:47.9,49.23 3 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:49.23,52.10 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:55.9,58.23 4 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:58.23,61.10 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers/visual.go:64.9,70.44 3 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes/routes.go:29.44,34.2 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes/routes.go:36.44,66.55 13 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes/routes.go:66.55,67.46 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes/routes.go:67.46,70.74 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes/routes.go:70.74,72.5 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes/routes.go:74.4,75.65 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes/routes.go:78.2,78.11 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes/routes.go:82.39,84.2 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:33.53,35.2 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:37.67,58.19 5 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:58.19,60.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:62.5,63.19 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:63.19,65.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:67.5,72.19 5 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:72.19,74.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:75.5,78.72 3 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:78.72,80.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:82.5,82.35 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:82.35,84.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:86.5,86.52 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:25.57,27.2 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:29.77,32.19 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:32.19,34.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:35.5,43.19 5 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:43.19,45.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:48.5,48.49 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:48.49,50.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:53.5,58.19 4 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:58.19,60.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:62.5,67.19 4 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:67.19,69.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:70.5,74.72 3 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:74.72,76.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:79.5,80.53 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:80.53,82.6 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:84.5,84.24 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:25.13,31.62 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:31.62,32.31 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:32.31,33.27 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:34.22,38.8 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:41.5,41.13 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:45.2,45.25 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:48.66,49.39 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:49.39,50.20 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:50.20,52.4 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:53.3,55.26 2 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:55.26,57.4 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:13.69,14.46 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:14.46,15.72 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:15.72,32.47 5 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:32.47,34.5 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:36.4,36.80 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:36.80,38.5 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:38.10,40.5 1 0
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:13.43,16.2 2 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:18.38,19.63 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:19.63,21.3 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:22.2,22.12 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:25.76,30.2 4 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:32.100,35.2 2 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:19.25,23.2 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:25.36,27.16 2 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:27.16,29.3 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:30.2,30.11 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:33.91,37.45 3 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:37.45,39.3 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:41.2,41.54 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:41.54,43.3 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:45.2,47.16 3 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:50.100,51.46 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:51.46,52.72 1 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:52.72,56.24 3 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:56.24,59.5 2 1
|
||||||
|
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:61.4,61.24 1 1
|
||||||
20
go.mod
20
go.mod
@@ -1,3 +1,23 @@
|
|||||||
module gitea.miguelmuniz.com/rogueking/junk2jive-server
|
module gitea.miguelmuniz.com/rogueking/junk2jive-server
|
||||||
|
|
||||||
go 1.24.2
|
go 1.24.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
|
github.com/go-chi/cors v1.2.1
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/lmittmann/tint v1.0.7
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.org/x/time v0.11.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/kr/pretty v0.3.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|||||||
41
go.sum
Normal file
41
go.sum
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
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/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y=
|
||||||
|
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||||
|
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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
|
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -1,26 +1,56 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
OpenAIKey string
|
OllamaKey string
|
||||||
RoboflowKey string
|
RoboflowKey string
|
||||||
Port string
|
Port string
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() *Config {
|
type Flags struct {
|
||||||
return &Config{
|
OllamaKey string
|
||||||
OpenAIKey: os.Getenv("OPENAI_API_KEY"),
|
RoboflowKey int
|
||||||
RoboflowKey: os.Getenv("ROBOFLOW_API_KEY"),
|
Port string
|
||||||
Port: getEnvWithDefault("PORT", "8080"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnvWithDefault(key, defaultValue string) string {
|
func LoadEnv(filePath string) {
|
||||||
if value := os.Getenv(key); value != "" {
|
if err := godotenv.Load(filePath); err != nil {
|
||||||
return value
|
logger.Fatal("Error loading .env file")
|
||||||
}
|
}
|
||||||
return defaultValue
|
}
|
||||||
}
|
|
||||||
|
func DeclareFlags() *Flags {
|
||||||
|
env := flag.String("e", "development", "Set the environment ( development | production )")
|
||||||
|
port := flag.Int("p", 0, "Set the port that will be used")
|
||||||
|
envFile := flag.String("ef", "", "Set the imported env file")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
return &Flags{
|
||||||
|
OllamaKey: *env,
|
||||||
|
RoboflowKey: *port,
|
||||||
|
Port: *envFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigPort(flags *Flags) string {
|
||||||
|
var port string
|
||||||
|
if flags.Port != 0 {
|
||||||
|
port = fmt.Sprintf(":%d", flags.Port)
|
||||||
|
} else {
|
||||||
|
if os.Getenv("APP_PORT") == "" {
|
||||||
|
logger.Warn("No port specified, default port :80 used...")
|
||||||
|
return ":80"
|
||||||
|
}
|
||||||
|
port = fmt.Sprintf(":%s", os.Getenv("APP_PORT"))
|
||||||
|
}
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"gitea.miguelmuniz.com/junk2jive-server/internal/config"
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config"
|
||||||
"gitea.miguelmuniz.com/junk2jive-server/internal/services"
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TextPromptRequest struct {
|
type TextPromptRequest struct {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.miguelmuniz.com/junk2jive-server/internal/config"
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config"
|
||||||
"gitea.miguelmuniz.com/junk2jive-server/internal/services"
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
func VisualAIHandler(cfg *config.Config) http.HandlerFunc {
|
func VisualAIHandler(cfg *config.Config) http.HandlerFunc {
|
||||||
|
|||||||
64
internal/limiter/limiter.go
Normal file
64
internal/limiter/limiter.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package limiter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RateLimiter struct {
|
||||||
|
limiters map[string]map[string]*rate.Limiter // map[path][ip] => limiter
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *RateLimiter {
|
||||||
|
return &RateLimiter{
|
||||||
|
limiters: make(map[string]map[string]*rate.Limiter),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIP(r *http.Request) string {
|
||||||
|
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) Get(path string, ip string, r rate.Limit, burst int) *rate.Limiter {
|
||||||
|
rl.mu.Lock()
|
||||||
|
defer rl.mu.Unlock()
|
||||||
|
|
||||||
|
if _, exists := rl.limiters[path]; !exists {
|
||||||
|
rl.limiters[path] = make(map[string]*rate.Limiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if limiter, exists := rl.limiters[path][ip]; exists {
|
||||||
|
return limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
limiter := rate.NewLimiter(r, burst)
|
||||||
|
rl.limiters[path][ip] = limiter
|
||||||
|
return limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) Middleware(rateLimit rate.Limit, burst int) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ip := GetIP(r)
|
||||||
|
limiter := rl.Get(r.URL.Path, ip, rateLimit, burst)
|
||||||
|
|
||||||
|
if !limiter.Allow() {
|
||||||
|
response.RespondWithError(w, r, http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests), fmt.Errorf("too many requests"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
136
internal/limiter/limiter_test.go
Normal file
136
internal/limiter/limiter_test.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package limiter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetIP(t *testing.T) {
|
||||||
|
req1 := &http.Request{RemoteAddr: "192.168.1.100:12345"}
|
||||||
|
ip1 := GetIP(req1)
|
||||||
|
assert.Equal(t, "192.168.1.100", ip1)
|
||||||
|
|
||||||
|
req2 := &http.Request{RemoteAddr: "invalid-address"}
|
||||||
|
ip2 := GetIP(req2)
|
||||||
|
assert.Equal(t, "invalid-address", ip2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetLimiter(t *testing.T) {
|
||||||
|
rl := New()
|
||||||
|
|
||||||
|
limiter1 := rl.Get("/test", "127.0.0.1", 1, 1)
|
||||||
|
require.NotNil(t, limiter1)
|
||||||
|
|
||||||
|
limiter2 := rl.Get("/test", "127.0.0.1", 1, 1)
|
||||||
|
assert.Same(t, limiter1, limiter2)
|
||||||
|
|
||||||
|
limiter3 := rl.Get("/another", "127.0.0.1", 1, 1)
|
||||||
|
assert.NotSame(t, limiter1, limiter3)
|
||||||
|
|
||||||
|
limiter4 := rl.Get("/test", "192.168.0.1", 1, 1)
|
||||||
|
assert.NotSame(t, limiter1, limiter4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware(t *testing.T) {
|
||||||
|
rl := New()
|
||||||
|
rateLimit := rate.Limit(1)
|
||||||
|
burst := 1
|
||||||
|
|
||||||
|
var nextCalled int32
|
||||||
|
|
||||||
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
atomic.StoreInt32(&nextCalled, 1)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("OK"))
|
||||||
|
})
|
||||||
|
|
||||||
|
handler := rl.Middleware(rateLimit, burst)(nextHandler)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
req.RemoteAddr = net.JoinHostPort("127.0.0.1", "12345")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
atomic.StoreInt32(&nextCalled, 0)
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
assert.Equal(t, int32(1), atomic.LoadInt32(&nextCalled))
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
atomic.StoreInt32(&nextCalled, 0)
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.NotEqual(t, http.StatusOK, rr.Code)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusTooManyRequests, rr.Code)
|
||||||
|
assert.Equal(t, int32(0), atomic.LoadInt32(&nextCalled))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareIndependent(t *testing.T) {
|
||||||
|
rl := New()
|
||||||
|
rateLimit := rate.Limit(1)
|
||||||
|
burst := 1
|
||||||
|
|
||||||
|
var handlerCallCount int32
|
||||||
|
|
||||||
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
atomic.AddInt32(&handlerCallCount, 1)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := rl.Middleware(rateLimit, burst)
|
||||||
|
|
||||||
|
req1 := httptest.NewRequest(http.MethodGet, "/path1", nil)
|
||||||
|
req1.RemoteAddr = net.JoinHostPort("127.0.0.1", "1111")
|
||||||
|
|
||||||
|
req2 := httptest.NewRequest(http.MethodGet, "/path2", nil)
|
||||||
|
req2.RemoteAddr = net.JoinHostPort("127.0.0.2", "2222")
|
||||||
|
|
||||||
|
rr1 := httptest.NewRecorder()
|
||||||
|
middleware(nextHandler).ServeHTTP(rr1, req1)
|
||||||
|
assert.Equal(t, http.StatusOK, rr1.Code)
|
||||||
|
|
||||||
|
rr2 := httptest.NewRecorder()
|
||||||
|
middleware(nextHandler).ServeHTTP(rr2, req2)
|
||||||
|
assert.Equal(t, http.StatusOK, rr2.Code)
|
||||||
|
|
||||||
|
assert.Equal(t, int32(2), atomic.LoadInt32(&handlerCallCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLimiterConcurrency(t *testing.T) {
|
||||||
|
rl := New()
|
||||||
|
path := "/concurrent"
|
||||||
|
ip := "127.0.0.1"
|
||||||
|
|
||||||
|
const goroutines = 50
|
||||||
|
limiterCh := make(chan *rate.Limiter, goroutines)
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func() {
|
||||||
|
limiter := rl.Get(path, ip, 5, 10)
|
||||||
|
limiterCh <- limiter
|
||||||
|
doneCh <- struct{}{}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
<-doneCh
|
||||||
|
}
|
||||||
|
|
||||||
|
close(limiterCh)
|
||||||
|
var firstLimiter *rate.Limiter
|
||||||
|
for lim := range limiterCh {
|
||||||
|
if firstLimiter == nil {
|
||||||
|
firstLimiter = lim
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, firstLimiter, lim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
internal/logger/application.go
Normal file
59
internal/logger/application.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lmittmann/tint"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger *slog.Logger
|
||||||
|
|
||||||
|
const LevelFatal slog.Level = slog.LevelError + 4
|
||||||
|
|
||||||
|
var (
|
||||||
|
Debug = makeLogFunc(slog.LevelDebug)
|
||||||
|
Info = makeLogFunc(slog.LevelInfo)
|
||||||
|
Warn = makeLogFunc(slog.LevelWarn)
|
||||||
|
Error = makeLogFunc(slog.LevelError)
|
||||||
|
Fatal = makeLogFunc(LevelFatal)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
w := os.Stderr
|
||||||
|
logger = slog.New(
|
||||||
|
tint.NewHandler(w, &tint.Options{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
TimeFormat: time.RFC3339,
|
||||||
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
if a.Key == slog.LevelKey {
|
||||||
|
switch a.Value.Any() {
|
||||||
|
case LevelFatal:
|
||||||
|
return slog.Attr{
|
||||||
|
Key: slog.LevelKey,
|
||||||
|
Value: slog.StringValue("\x1b[91mFATAL\x1b[0m"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeLogFunc(level slog.Level) func(msg string, args ...any) {
|
||||||
|
return func(msg string, args ...any) {
|
||||||
|
if len(args) > 0 {
|
||||||
|
msg = fmt.Sprintf(msg, args...)
|
||||||
|
}
|
||||||
|
logger.Log(context.Background(), level, msg)
|
||||||
|
|
||||||
|
if level == LevelFatal {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
internal/logger/request.go
Normal file
43
internal/logger/request.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Middleware(logger *zap.Logger) 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()
|
||||||
|
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||||
|
|
||||||
|
next.ServeHTTP(ww, r)
|
||||||
|
|
||||||
|
logFields := []zap.Field{
|
||||||
|
zap.String("method", r.Method),
|
||||||
|
zap.String("path", r.URL.Path),
|
||||||
|
zap.String("query", r.URL.RawQuery),
|
||||||
|
zap.Int("status", ww.Status()),
|
||||||
|
zap.String("ip", r.RemoteAddr),
|
||||||
|
zap.String("user-agent", r.UserAgent()),
|
||||||
|
zap.Duration("latency", time.Since(start)),
|
||||||
|
zap.String("time", time.Now().Format(time.RFC3339)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := response.GetError(r); err != nil {
|
||||||
|
logFields = append(logFields, zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ww.Status() >= http.StatusBadRequest && ww.Status() != http.StatusTeapot {
|
||||||
|
logger.Error("Request failed", logFields...)
|
||||||
|
} else {
|
||||||
|
logger.Info("Request", logFields...)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
35
internal/response/response.go
Normal file
35
internal/response/response.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type errorKeyType struct{}
|
||||||
|
|
||||||
|
var errorContextKey = errorKeyType{}
|
||||||
|
|
||||||
|
func SetError(r *http.Request, err error) {
|
||||||
|
ctx := context.WithValue(r.Context(), errorContextKey, err)
|
||||||
|
*r = *r.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetError(r *http.Request) error {
|
||||||
|
if err, ok := r.Context().Value(errorContextKey).(error); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
|
||||||
|
response, _ := json.Marshal(payload)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(code)
|
||||||
|
w.Write(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RespondWithError(w http.ResponseWriter, r *http.Request, code int, message string, err error) {
|
||||||
|
SetError(r, err)
|
||||||
|
RespondWithJSON(w, code, map[string]string{"error": message})
|
||||||
|
}
|
||||||
65
internal/response/response_test.go
Normal file
65
internal/response/response_test.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetAndGetError(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("GET", "/test", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, GetError(req))
|
||||||
|
|
||||||
|
testErr := errors.New("test error")
|
||||||
|
SetError(req, testErr)
|
||||||
|
errFromReq := GetError(req)
|
||||||
|
require.NotNil(t, errFromReq)
|
||||||
|
assert.Equal(t, testErr.Error(), errFromReq.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRespondWithJSON(t *testing.T) {
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
payload := map[string]string{"message": "hello"}
|
||||||
|
code := http.StatusOK
|
||||||
|
|
||||||
|
RespondWithJSON(rr, code, payload)
|
||||||
|
|
||||||
|
assert.Equal(t, code, rr.Code)
|
||||||
|
assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
|
||||||
|
|
||||||
|
var data map[string]string
|
||||||
|
err := json.Unmarshal(rr.Body.Bytes(), &data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, payload, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRespondWithError(t *testing.T) {
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
req, err := http.NewRequest("GET", "/test", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
code := http.StatusBadRequest
|
||||||
|
message := "error occurred"
|
||||||
|
testErr := errors.New("test error")
|
||||||
|
|
||||||
|
RespondWithError(rr, req, code, message, testErr)
|
||||||
|
|
||||||
|
errFromReq := GetError(req)
|
||||||
|
require.NotNil(t, errFromReq)
|
||||||
|
assert.Equal(t, testErr.Error(), errFromReq.Error())
|
||||||
|
|
||||||
|
assert.Equal(t, code, rr.Code)
|
||||||
|
assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
|
||||||
|
|
||||||
|
var data map[string]string
|
||||||
|
err = json.Unmarshal(rr.Body.Bytes(), &data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected := map[string]string{"error": message}
|
||||||
|
assert.Equal(t, expected, data)
|
||||||
|
}
|
||||||
@@ -2,37 +2,83 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/go-chi/cors"
|
"github.com/go-chi/cors"
|
||||||
"gitea.miguelmuniz.com/junk2jive-server/internal/config"
|
"go.uber.org/zap"
|
||||||
"gitea.miguelmuniz.com/junk2jive-server/internal/handlers"
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config"
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers"
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter"
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger"
|
||||||
|
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupRoutes(cfg *config.Config) *chi.Mux {
|
// Router encapsulates the Chi router and its dependencies
|
||||||
r := chi.NewRouter()
|
type Router struct {
|
||||||
|
router *chi.Mux
|
||||||
|
config *config.Config
|
||||||
|
logger *zap.Logger
|
||||||
|
rateLimiter *limiter.RateLimiter
|
||||||
|
}
|
||||||
|
|
||||||
// Middleware
|
// NewRouter creates and configures a new Router instance
|
||||||
r.Use(middleware.Logger)
|
func NewRouter(cfg *config.Config) *Router {
|
||||||
r.Use(middleware.Recoverer)
|
return &Router{
|
||||||
r.Use(cors.Handler(cors.Options{
|
router: chi.NewRouter(),
|
||||||
AllowedOrigins: []string{"*"},
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupRouter(origins []string) *Router {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
|
||||||
|
router.Use(cors.Handler(cors.Options{
|
||||||
|
AllowedOrigins: origins,
|
||||||
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
||||||
AllowedHeaders: []string{"Accept", "Content-Type"},
|
AllowedHeaders: []string{"Content-Type", "X-CSRF-Token"},
|
||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
|
MaxAge: 300,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Static files
|
zlogger, _ := zap.NewProduction()
|
||||||
fileServer := http.FileServer(http.Dir("./static"))
|
defer zlogger.Sync()
|
||||||
r.Handle("/static/*", http.StripPrefix("/static", fileServer))
|
|
||||||
|
|
||||||
// API routes
|
rl := limiter.New()
|
||||||
r.Get("/", handlers.HomeHandler)
|
|
||||||
r.Route("/api", func(r chi.Router) {
|
router.Use(middleware.RequestID)
|
||||||
r.Post("/text-prompt", handlers.TextPromptHandler(cfg))
|
router.Use(middleware.RealIP)
|
||||||
r.Post("/ai-prompt", handlers.VisualAIHandler(cfg))
|
router.Use(middleware.Logger)
|
||||||
|
router.Use(middleware.Recoverer)
|
||||||
|
router.Use(middleware.Timeout(60 * time.Second))
|
||||||
|
|
||||||
|
router.Use(logger.Middleware(zlogger))
|
||||||
|
|
||||||
|
r := Router{
|
||||||
|
router: router,
|
||||||
|
logger: zlogger,
|
||||||
|
rateLimiter: rl,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.router.Route("/v1/api", func(apiRouter chi.Router) {
|
||||||
|
apiRouter.Group(func(subRouter chi.Router) {
|
||||||
|
subRouter.Use(r.rateLimiter.Middleware(rate.Every(1*time.Second), 30))
|
||||||
|
|
||||||
|
subRouter.Get("/coffee", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response.RespondWithJSON(w, http.StatusTeapot, map[string]string{"error": "I'm A Teapot!"})
|
||||||
|
})
|
||||||
|
|
||||||
|
subRouter.Post("/text", handlers.TextPromptHandler(r.config))
|
||||||
|
subRouter.Post("/visual", handlers.VisualAIHandler(r.config))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
return &r
|
||||||
|
|
||||||
return r
|
}
|
||||||
|
|
||||||
|
func (r *Router) GetRouter() *chi.Mux {
|
||||||
|
return r.router
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user