added workflows and fixed router
Some checks failed
Build and Push Docker Image / Build and push image (push) Failing after 43s
Run Go Tests / build (push) Failing after 0s
golangci-lint / lint (push) Failing after 2m0s

This commit is contained in:
2025-05-06 14:49:14 -07:00
parent e7b1090a85
commit 84e150cb11
18 changed files with 786 additions and 61 deletions

View 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

View 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

View 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:

View File

@@ -9,32 +9,10 @@ import (
"path/filepath"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/handlers"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/routes"
)
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
cfg, err := config.Load(configPath)

99
coverage.out Normal file
View 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
View File

@@ -1,3 +1,23 @@
module gitea.miguelmuniz.com/rogueking/junk2jive-server
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
View 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=

View File

@@ -1,26 +1,56 @@
package config
import (
"flag"
"fmt"
"os"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger"
"github.com/joho/godotenv"
)
type Config struct {
OpenAIKey string
OllamaKey string
RoboflowKey string
Port string
}
func LoadConfig() *Config {
return &Config{
OpenAIKey: os.Getenv("OPENAI_API_KEY"),
RoboflowKey: os.Getenv("ROBOFLOW_API_KEY"),
Port: getEnvWithDefault("PORT", "8080"),
type Flags struct {
OllamaKey string
RoboflowKey int
Port string
}
func LoadEnv(filePath string) {
if err := godotenv.Load(filePath); err != nil {
logger.Fatal("Error loading .env file")
}
}
func getEnvWithDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
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,
}
return defaultValue
}
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
}

View File

@@ -4,8 +4,8 @@ import (
"encoding/json"
"net/http"
"gitea.miguelmuniz.com/junk2jive-server/internal/config"
"gitea.miguelmuniz.com/junk2jive-server/internal/services"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services"
)
type TextPromptRequest struct {

View File

@@ -9,8 +9,8 @@ import (
"path/filepath"
"strings"
"gitea.miguelmuniz.com/junk2jive-server/internal/config"
"gitea.miguelmuniz.com/junk2jive-server/internal/services"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services"
)
func VisualAIHandler(cfg *config.Config) http.HandlerFunc {

View 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)
})
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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...)
}
})
}
}

View 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})
}

View 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)
}

View File

@@ -2,37 +2,83 @@ package routes
import (
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"gitea.miguelmuniz.com/junk2jive-server/internal/config"
"gitea.miguelmuniz.com/junk2jive-server/internal/handlers"
"go.uber.org/zap"
"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 {
r := chi.NewRouter()
// Router encapsulates the Chi router and its dependencies
type Router struct {
router *chi.Mux
config *config.Config
logger *zap.Logger
rateLimiter *limiter.RateLimiter
}
// Middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
// NewRouter creates and configures a new Router instance
func NewRouter(cfg *config.Config) *Router {
return &Router{
router: chi.NewRouter(),
config: cfg,
}
}
func SetupRouter(origins []string) *Router {
router := chi.NewRouter()
router.Use(cors.Handler(cors.Options{
AllowedOrigins: origins,
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Content-Type"},
AllowedHeaders: []string{"Content-Type", "X-CSRF-Token"},
AllowCredentials: true,
MaxAge: 300,
}))
// Static files
fileServer := http.FileServer(http.Dir("./static"))
r.Handle("/static/*", http.StripPrefix("/static", fileServer))
zlogger, _ := zap.NewProduction()
defer zlogger.Sync()
// API routes
r.Get("/", handlers.HomeHandler)
r.Route("/api", func(r chi.Router) {
r.Post("/text-prompt", handlers.TextPromptHandler(cfg))
r.Post("/ai-prompt", handlers.VisualAIHandler(cfg))
rl := limiter.New()
router.Use(middleware.RequestID)
router.Use(middleware.RealIP)
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!"})
})
return r
subRouter.Post("/text", handlers.TextPromptHandler(r.config))
subRouter.Post("/visual", handlers.VisualAIHandler(r.config))
})
})
return &r
}
func (r *Router) GetRouter() *chi.Mux {
return r.router
}