diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d501674 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,42 @@ +version: 2 + +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: weekly + day: saturday + groups: + go-dependencies: + update-types: + - minor + - patch + labels: + - dependencies + + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + day: saturday + groups: + docker-images: + update-types: + - minor + - patch + labels: + - dependencies + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: saturday + groups: + github-actions: + update-types: + - minor + - patch + labels: + - dependencies + - area:ci diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..2dc7192 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,25 @@ +name: Dependabot auto-merge + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + if: github.event.pull_request.user.login == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Fetch Dependabot metadata + id: meta + uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable auto-merge for non-major updates + if: steps.meta.outputs.update-type != 'version-update:semver-major' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..f624b88 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,8 @@ +[extend] +useDefault = true + +[[allowlists]] +description = "Dummy credentials in Go unit tests" +paths = [ + '''_test\.go$''', +] diff --git a/.golangci.yml b/.golangci.yml index 4bcfd88..5021a0f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,28 +1,44 @@ +version: "2" run: - timeout: 5m go: "1.26" - linters: - disable-all: true + default: none enable: + - bodyclose - errcheck + - gosec - govet - ineffassign + - misspell + - nilerr + - revive + - rowserrcheck - staticcheck - unused + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - gosec + path: _test\.go + paths: + - internal/gen + - third_party$ + - builtin$ + - examples$ +formatters: + enable: - gofmt - goimports - - gosec - - misspell - - revive - - bodyclose - - nilerr - - rowserrcheck - -issues: - exclude-dirs: - - internal/gen - exclude-rules: - - path: _test\.go - linters: - - gosec + exclusions: + generated: lax + paths: + - internal/gen + - third_party$ + - builtin$ + - examples$ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5b4250f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.26@sha256:2d6c80227255c3112a4d08e67ba98e58efd3846daf15d9d7d4c389565d881b1a AS build +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/api-proxy ./cmd/api-proxy + +FROM gcr.io/distroless/static-debian13:nonroot@sha256:963fa6c544fe5ce420f1f54fb88b6fb01479f054c8056d0f74cc2c6000df5240 +COPY --from=build /out/api-proxy /api-proxy +EXPOSE 8080 +USER nonroot:nonroot +ENTRYPOINT ["/api-proxy"] diff --git a/cmd/api-proxy/main.go b/cmd/api-proxy/main.go index 73322c7..3ede56b 100644 --- a/cmd/api-proxy/main.go +++ b/cmd/api-proxy/main.go @@ -4,6 +4,7 @@ import ( "log/slog" "net/http" "os" + "time" ) func healthz(w http.ResponseWriter, _ *http.Request) { @@ -20,7 +21,12 @@ func main() { } logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger.Info("api-proxy starting", "addr", addr) - if err := http.ListenAndServe(addr, mux); err != nil { + srv := &http.Server{ + Addr: addr, + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, + } + if err := srv.ListenAndServe(); err != nil { logger.Error("server exited", "err", err) os.Exit(1) } diff --git a/internal/auth/jwt_test.go b/internal/auth/jwt_test.go index 09b5c14..b340966 100644 --- a/internal/auth/jwt_test.go +++ b/internal/auth/jwt_test.go @@ -8,9 +8,9 @@ import ( func newTestService(t *testing.T) *Service { t.Helper() return NewService(Options{ - SignKey: []byte("01234567890123456789012345678901"), - Clock: func() time.Time { return time.Unix(1_700_000_000, 0) }, - AccessTTL: time.Hour, + SignKey: []byte("01234567890123456789012345678901"), + Clock: func() time.Time { return time.Unix(1_700_000_000, 0) }, + AccessTTL: time.Hour, }) } diff --git a/internal/auth/lockout_test.go b/internal/auth/lockout_test.go index e0db285..14b6616 100644 --- a/internal/auth/lockout_test.go +++ b/internal/auth/lockout_test.go @@ -23,7 +23,7 @@ func newLockoutSvc(t *testing.T, now time.Time) *Service { d := openTestDB(t) return NewService(Options{ DB: d.DB, - SignKey: []byte("01234567890123456789012345678901"), + SignKey: []byte("01234567890123456789012345678901"), Clock: func() time.Time { return now }, }) } diff --git a/internal/auth/login.go b/internal/auth/login.go index 825cc56..cda2155 100644 --- a/internal/auth/login.go +++ b/internal/auth/login.go @@ -119,4 +119,3 @@ func sha256Hex(s string) string { sum := sha256.Sum256([]byte(s)) return hex.EncodeToString(sum[:]) } - diff --git a/internal/auth/login_test.go b/internal/auth/login_test.go index a81f5d1..ee5d7fb 100644 --- a/internal/auth/login_test.go +++ b/internal/auth/login_test.go @@ -28,7 +28,7 @@ func newLoginSvc(t *testing.T, jf JellyfinAuthenticator) *Service { return NewService(Options{ DB: d.DB, Jellyfin: jf, - SignKey: []byte("01234567890123456789012345678901"), + SignKey: []byte("01234567890123456789012345678901"), Clock: func() time.Time { return time.Unix(1_700_000_000, 0) }, }) } diff --git a/internal/auth/service.go b/internal/auth/service.go index a9bc9bf..36d6848 100644 --- a/internal/auth/service.go +++ b/internal/auth/service.go @@ -37,10 +37,10 @@ type Service struct { type Options struct { DB *sql.DB Jellyfin JellyfinAuthenticator - SignKey []byte - Clock Clock - AccessTTL time.Duration - RefreshTTL time.Duration + SignKey []byte + Clock Clock + AccessTTL time.Duration + RefreshTTL time.Duration } func NewService(opts Options) *Service { diff --git a/internal/clients/jellyfin/quickconnect_test.go b/internal/clients/jellyfin/quickconnect_test.go index e25d039..8ad3d7c 100644 --- a/internal/clients/jellyfin/quickconnect_test.go +++ b/internal/clients/jellyfin/quickconnect_test.go @@ -39,7 +39,7 @@ func TestQuickConnectAuthenticate_Approved(t *testing.T) { } _ = json.NewEncoder(w).Encode(map[string]any{ "AccessToken": "tok-qc", - "User": map[string]any{"Id": "jf-user-1", "Name": "alice"}, + "User": map[string]any{"Id": "jf-user-1", "Name": "alice"}, }) })) defer s.Close() diff --git a/internal/db/db.go b/internal/db/db.go index 51d0155..b105e9e 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -6,7 +6,7 @@ import ( "embed" "fmt" - _ "modernc.org/sqlite" + _ "modernc.org/sqlite" // registers the "sqlite" database/sql driver ) //go:embed migrations/*.sql