From 3e59b8d98ee75dc2050505dcc858114d575fa971 Mon Sep 17 00:00:00 2001 From: Aaro Koinsaari <89689072+koinsaari@users.noreply.github.com> Date: Sat, 30 May 2026 00:06:15 +0300 Subject: [PATCH] chore: strict-server codegen, CI improvements, remove auto-assign - Enable strict-server mode in oapi-codegen.yaml (typed request/response objects, no manual json.Decoder/Encoder in handlers) - Remove internal/gen/ from .gitignore and .dockerignore so the generated file is tracked and the Docker build context is self-contained - Add .claudeignore to skip indexing internal/gen/ and go.sum (save context tokens) - Add codegen-check CI job with go mod download pre-warm to detect spec/generated-code drift on every PR - Bump setup-go to v6.4.0 across all CI jobs - Remove auto-assign workflow (noise on every PR edit, not needed for small team) Co-Authored-By: Claude Sonnet 4.6 --- .claudeignore | 2 + .dockerignore | 1 - .github/workflows/auto-assign.yml | 33 - .github/workflows/ci.yml | 24 +- .gitignore | 3 - internal/gen/api.gen.go | 1022 +++++++++++++++++++++++++++++ oapi-codegen.yaml | 1 + 7 files changed, 1047 insertions(+), 39 deletions(-) create mode 100644 .claudeignore delete mode 100644 .github/workflows/auto-assign.yml create mode 100644 internal/gen/api.gen.go diff --git a/.claudeignore b/.claudeignore new file mode 100644 index 0000000..9354500 --- /dev/null +++ b/.claudeignore @@ -0,0 +1,2 @@ +internal/gen/ +go.sum diff --git a/.dockerignore b/.dockerignore index 8c995cd..0b4d7c0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,5 @@ docs .github dist compose -internal/gen coverage.out *.sqlite diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml deleted file mode 100644 index 841cac3..0000000 --- a/.github/workflows/auto-assign.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Auto-assign - -on: - pull_request: - types: [opened, reopened, edited] - -permissions: - issues: write - pull-requests: write - -jobs: - assign: - name: Assign PR author to PR and linked issues - runs-on: ubuntu-latest - steps: - - name: Assign author to the PR - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - AUTHOR: ${{ github.event.pull_request.user.login }} - REPO: ${{ github.repository }} - run: gh pr edit "$PR_NUMBER" --repo "$REPO" --add-assignee "$AUTHOR" - - - name: Assign author to linked issues - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BODY: ${{ github.event.pull_request.body }} - AUTHOR: ${{ github.event.pull_request.user.login }} - REPO: ${{ github.repository }} - run: | - echo "$BODY" | grep -oiE '(closes|fixes|resolves) #[0-9]+' | grep -oE '[0-9]+' | while read -r num; do - gh issue edit "$num" --repo "$REPO" --add-assignee "$AUTHOR" - done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b795d83..5efbaaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 1 - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod @@ -39,7 +39,7 @@ jobs: fetch-depth: 1 - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod @@ -48,6 +48,26 @@ jobs: with: version: latest + codegen-check: + name: codegen check + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 1 + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: go.mod + + - name: Regenerate and check for drift + run: | + go mod download + make gen + git diff --exit-code internal/gen/ + docker-build: name: docker build runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 63f5982..fcee432 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,6 @@ /api-proxy /dist/ -# generated (regenerated by `make gen`; not committed) -/internal/gen/ - # env / secrets .env .env.local diff --git a/internal/gen/api.gen.go b/internal/gen/api.gen.go new file mode 100644 index 0000000..0f2f5bc --- /dev/null +++ b/internal/gen/api.gen.go @@ -0,0 +1,1022 @@ +//go:build go1.22 + +// Package gen provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.7.0 DO NOT EDIT. +package gen + +import ( + "bytes" + "compress/flate" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +const ( + BearerJWTScopes bearerJWTContextKey = "BearerJWT.Scopes" +) + +// Defines values for ErrorErrorCode. +const ( + AccountLocked ErrorErrorCode = "account_locked" + BackendUnavailable ErrorErrorCode = "backend_unavailable" + Internal ErrorErrorCode = "internal" + InvalidCredentials ErrorErrorCode = "invalid_credentials" + JellyfinSessionExpired ErrorErrorCode = "jellyfin_session_expired" + RateLimited ErrorErrorCode = "rate_limited" + TokenExpired ErrorErrorCode = "token_expired" + TokenInvalid ErrorErrorCode = "token_invalid" + ValidationFailed ErrorErrorCode = "validation_failed" +) + +// Valid indicates whether the value is a known member of the ErrorErrorCode enum. +func (e ErrorErrorCode) Valid() bool { + switch e { + case AccountLocked: + return true + case BackendUnavailable: + return true + case Internal: + return true + case InvalidCredentials: + return true + case JellyfinSessionExpired: + return true + case RateLimited: + return true + case TokenExpired: + return true + case TokenInvalid: + return true + case ValidationFailed: + return true + default: + return false + } +} + +// Defines values for GetHealthz200JSONResponseBodyStatus. +const ( + Ok GetHealthz200JSONResponseBodyStatus = "ok" +) + +// Valid indicates whether the value is a known member of the GetHealthz200JSONResponseBodyStatus enum. +func (e GetHealthz200JSONResponseBodyStatus) Valid() bool { + switch e { + case Ok: + return true + default: + return false + } +} + +// Error defines model for Error. +type Error struct { + Error struct { + Code ErrorErrorCode `json:"code"` + Details interface{} `json:"details,omitempty"` + Message string `json:"message"` + } `json:"error"` + RequestId string `json:"request_id"` +} + +// ErrorErrorCode defines model for Error.Error.Code. +type ErrorErrorCode string + +// LoginRequest defines model for LoginRequest. +type LoginRequest struct { + DeviceLabel *string `json:"device_label,omitempty"` + Password string `json:"password"` + Username string `json:"username"` +} + +// QuickConnectPollRequest defines model for QuickConnectPollRequest. +type QuickConnectPollRequest struct { + PollToken string `json:"poll_token"` +} + +// QuickConnectStartResponse defines model for QuickConnectStartResponse. +type QuickConnectStartResponse struct { + Code string `json:"code"` + PollToken string `json:"poll_token"` +} + +// RefreshRequest defines model for RefreshRequest. +type RefreshRequest struct { + RefreshToken string `json:"refresh_token"` +} + +// TokenPair defines model for TokenPair. +type TokenPair struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + User User `json:"user"` +} + +// User defines model for User. +type User struct { + DisplayName string `json:"display_name"` + Email string `json:"email"` + Id string `json:"id"` +} + +// bearerJWTContextKey is the context key for BearerJWT security scheme +type bearerJWTContextKey string + +// GetHealthz200JSONResponseBodyStatus defines parameters for GetHealthz. +type GetHealthz200JSONResponseBodyStatus string + +// PostAuthLoginJSONRequestBody defines body for PostAuthLogin for application/json ContentType. +type PostAuthLoginJSONRequestBody = LoginRequest + +// PostAuthLogoutJSONRequestBody defines body for PostAuthLogout for application/json ContentType. +type PostAuthLogoutJSONRequestBody = RefreshRequest + +// PostAuthQuickConnectPollJSONRequestBody defines body for PostAuthQuickConnectPoll for application/json ContentType. +type PostAuthQuickConnectPollJSONRequestBody = QuickConnectPollRequest + +// PostAuthRefreshJSONRequestBody defines body for PostAuthRefresh for application/json ContentType. +type PostAuthRefreshJSONRequestBody = RefreshRequest + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Username + password login (proxied through Jellyfin AuthenticateByName) + // (POST /auth/login) + PostAuthLogin(w http.ResponseWriter, r *http.Request) + // Revoke a refresh token + // (POST /auth/logout) + PostAuthLogout(w http.ResponseWriter, r *http.Request) + // Revoke all refresh tokens for the user + // (POST /auth/logout/all) + PostAuthLogoutAll(w http.ResponseWriter, r *http.Request) + // Poll Quick Connect approval + // (POST /auth/quick-connect/poll) + PostAuthQuickConnectPoll(w http.ResponseWriter, r *http.Request) + // Begin a Jellyfin Quick Connect handshake + // (POST /auth/quick-connect/start) + PostAuthQuickConnectStart(w http.ResponseWriter, r *http.Request) + // Refresh token rotation + // (POST /auth/refresh) + PostAuthRefresh(w http.ResponseWriter, r *http.Request) + // Liveness probe + // (GET /healthz) + GetHealthz(w http.ResponseWriter, r *http.Request) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// PostAuthLogin operation middleware +func (siw *ServerInterfaceWrapper) PostAuthLogin(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostAuthLogin(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PostAuthLogout operation middleware +func (siw *ServerInterfaceWrapper) PostAuthLogout(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerJWTScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostAuthLogout(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PostAuthLogoutAll operation middleware +func (siw *ServerInterfaceWrapper) PostAuthLogoutAll(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerJWTScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostAuthLogoutAll(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PostAuthQuickConnectPoll operation middleware +func (siw *ServerInterfaceWrapper) PostAuthQuickConnectPoll(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostAuthQuickConnectPoll(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PostAuthQuickConnectStart operation middleware +func (siw *ServerInterfaceWrapper) PostAuthQuickConnectStart(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostAuthQuickConnectStart(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PostAuthRefresh operation middleware +func (siw *ServerInterfaceWrapper) PostAuthRefresh(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PostAuthRefresh(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetHealthz operation middleware +func (siw *ServerInterfaceWrapper) GetHealthz(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetHealthz(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshalingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshalingParamError) Error() string { + return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshalingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{}) +} + +// ServeMux is an abstraction of [http.ServeMux]. +type ServeMux interface { + HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) + http.Handler +} + +type StdHTTPServerOptions struct { + BaseURL string + BaseRouter ServeMux + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, m ServeMux) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{ + BaseRouter: m, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, m ServeMux, baseURL string) http.Handler { + return HandlerWithOptions(si, StdHTTPServerOptions{ + BaseURL: baseURL, + BaseRouter: m, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.Handler { + m := options.BaseRouter + + if m == nil { + m = http.NewServeMux() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/auth/login", wrapper.PostAuthLogin) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/auth/logout", wrapper.PostAuthLogout) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/auth/logout/all", wrapper.PostAuthLogoutAll) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/auth/quick-connect/poll", wrapper.PostAuthQuickConnectPoll) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/auth/quick-connect/start", wrapper.PostAuthQuickConnectStart) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/auth/refresh", wrapper.PostAuthRefresh) + m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/healthz", wrapper.GetHealthz) + + return m +} + +type PostAuthLoginRequestObject struct { + Body *PostAuthLoginJSONRequestBody +} + +type PostAuthLoginResponseObject interface { + VisitPostAuthLoginResponse(w http.ResponseWriter) error +} + +type PostAuthLogin200JSONResponse TokenPair + +func (response PostAuthLogin200JSONResponse) VisitPostAuthLoginResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type PostAuthLogin401JSONResponse Error + +func (response PostAuthLogin401JSONResponse) VisitPostAuthLoginResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + _, err := buf.WriteTo(w) + return err +} + +type PostAuthLogin423JSONResponse Error + +func (response PostAuthLogin423JSONResponse) VisitPostAuthLoginResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(423) + _, err := buf.WriteTo(w) + return err +} + +type PostAuthLogin503JSONResponse Error + +func (response PostAuthLogin503JSONResponse) VisitPostAuthLoginResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(503) + _, err := buf.WriteTo(w) + return err +} + +type PostAuthLogoutRequestObject struct { + Body *PostAuthLogoutJSONRequestBody +} + +type PostAuthLogoutResponseObject interface { + VisitPostAuthLogoutResponse(w http.ResponseWriter) error +} + +type PostAuthLogout204Response struct { +} + +func (response PostAuthLogout204Response) VisitPostAuthLogoutResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type PostAuthLogoutAllRequestObject struct { +} + +type PostAuthLogoutAllResponseObject interface { + VisitPostAuthLogoutAllResponse(w http.ResponseWriter) error +} + +type PostAuthLogoutAll204Response struct { +} + +func (response PostAuthLogoutAll204Response) VisitPostAuthLogoutAllResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type PostAuthQuickConnectPollRequestObject struct { + Body *PostAuthQuickConnectPollJSONRequestBody +} + +type PostAuthQuickConnectPollResponseObject interface { + VisitPostAuthQuickConnectPollResponse(w http.ResponseWriter) error +} + +type PostAuthQuickConnectPoll200JSONResponse TokenPair + +func (response PostAuthQuickConnectPoll200JSONResponse) VisitPostAuthQuickConnectPollResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type PostAuthQuickConnectPoll202Response struct { +} + +func (response PostAuthQuickConnectPoll202Response) VisitPostAuthQuickConnectPollResponse(w http.ResponseWriter) error { + w.WriteHeader(202) + return nil +} + +type PostAuthQuickConnectPoll410JSONResponse Error + +func (response PostAuthQuickConnectPoll410JSONResponse) VisitPostAuthQuickConnectPollResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(410) + _, err := buf.WriteTo(w) + return err +} + +type PostAuthQuickConnectStartRequestObject struct { +} + +type PostAuthQuickConnectStartResponseObject interface { + VisitPostAuthQuickConnectStartResponse(w http.ResponseWriter) error +} + +type PostAuthQuickConnectStart200JSONResponse QuickConnectStartResponse + +func (response PostAuthQuickConnectStart200JSONResponse) VisitPostAuthQuickConnectStartResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type PostAuthQuickConnectStart503JSONResponse Error + +func (response PostAuthQuickConnectStart503JSONResponse) VisitPostAuthQuickConnectStartResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(503) + _, err := buf.WriteTo(w) + return err +} + +type PostAuthRefreshRequestObject struct { + Body *PostAuthRefreshJSONRequestBody +} + +type PostAuthRefreshResponseObject interface { + VisitPostAuthRefreshResponse(w http.ResponseWriter) error +} + +type PostAuthRefresh200JSONResponse TokenPair + +func (response PostAuthRefresh200JSONResponse) VisitPostAuthRefreshResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +type PostAuthRefresh401JSONResponse Error + +func (response PostAuthRefresh401JSONResponse) VisitPostAuthRefreshResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + _, err := buf.WriteTo(w) + return err +} + +type GetHealthzRequestObject struct { +} + +type GetHealthzResponseObject interface { + VisitGetHealthzResponse(w http.ResponseWriter) error +} + +type GetHealthz200JSONResponse struct { + Status GetHealthz200JSONResponseBodyStatus `json:"status"` +} + +func (response GetHealthz200JSONResponse) VisitGetHealthzResponse(w http.ResponseWriter) error { + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(response); err != nil { + return err + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + _, err := buf.WriteTo(w) + return err +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // Username + password login (proxied through Jellyfin AuthenticateByName) + // (POST /auth/login) + PostAuthLogin(ctx context.Context, request PostAuthLoginRequestObject) (PostAuthLoginResponseObject, error) + // Revoke a refresh token + // (POST /auth/logout) + PostAuthLogout(ctx context.Context, request PostAuthLogoutRequestObject) (PostAuthLogoutResponseObject, error) + // Revoke all refresh tokens for the user + // (POST /auth/logout/all) + PostAuthLogoutAll(ctx context.Context, request PostAuthLogoutAllRequestObject) (PostAuthLogoutAllResponseObject, error) + // Poll Quick Connect approval + // (POST /auth/quick-connect/poll) + PostAuthQuickConnectPoll(ctx context.Context, request PostAuthQuickConnectPollRequestObject) (PostAuthQuickConnectPollResponseObject, error) + // Begin a Jellyfin Quick Connect handshake + // (POST /auth/quick-connect/start) + PostAuthQuickConnectStart(ctx context.Context, request PostAuthQuickConnectStartRequestObject) (PostAuthQuickConnectStartResponseObject, error) + // Refresh token rotation + // (POST /auth/refresh) + PostAuthRefresh(ctx context.Context, request PostAuthRefreshRequestObject) (PostAuthRefreshResponseObject, error) + // Liveness probe + // (GET /healthz) + GetHealthz(ctx context.Context, request GetHealthzRequestObject) (GetHealthzResponseObject, error) +} + +type StrictHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) +type StrictMiddlewareFunc func(f StrictHandlerFunc, operationID string) StrictHandlerFunc + +type StrictHTTPServerOptions struct { + RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ + RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + }, + ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }, + }} +} + +func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc + options StrictHTTPServerOptions +} + +// PostAuthLogin operation middleware +func (sh *strictHandler) PostAuthLogin(w http.ResponseWriter, r *http.Request) { + var request PostAuthLoginRequestObject + + var body PostAuthLoginJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostAuthLogin(ctx, request.(PostAuthLoginRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostAuthLogin") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostAuthLoginResponseObject); ok { + if err := validResponse.VisitPostAuthLoginResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PostAuthLogout operation middleware +func (sh *strictHandler) PostAuthLogout(w http.ResponseWriter, r *http.Request) { + var request PostAuthLogoutRequestObject + + var body PostAuthLogoutJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostAuthLogout(ctx, request.(PostAuthLogoutRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostAuthLogout") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostAuthLogoutResponseObject); ok { + if err := validResponse.VisitPostAuthLogoutResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PostAuthLogoutAll operation middleware +func (sh *strictHandler) PostAuthLogoutAll(w http.ResponseWriter, r *http.Request) { + var request PostAuthLogoutAllRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostAuthLogoutAll(ctx, request.(PostAuthLogoutAllRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostAuthLogoutAll") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostAuthLogoutAllResponseObject); ok { + if err := validResponse.VisitPostAuthLogoutAllResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PostAuthQuickConnectPoll operation middleware +func (sh *strictHandler) PostAuthQuickConnectPoll(w http.ResponseWriter, r *http.Request) { + var request PostAuthQuickConnectPollRequestObject + + var body PostAuthQuickConnectPollJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostAuthQuickConnectPoll(ctx, request.(PostAuthQuickConnectPollRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostAuthQuickConnectPoll") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostAuthQuickConnectPollResponseObject); ok { + if err := validResponse.VisitPostAuthQuickConnectPollResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PostAuthQuickConnectStart operation middleware +func (sh *strictHandler) PostAuthQuickConnectStart(w http.ResponseWriter, r *http.Request) { + var request PostAuthQuickConnectStartRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostAuthQuickConnectStart(ctx, request.(PostAuthQuickConnectStartRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostAuthQuickConnectStart") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostAuthQuickConnectStartResponseObject); ok { + if err := validResponse.VisitPostAuthQuickConnectStartResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PostAuthRefresh operation middleware +func (sh *strictHandler) PostAuthRefresh(w http.ResponseWriter, r *http.Request) { + var request PostAuthRefreshRequestObject + + var body PostAuthRefreshJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostAuthRefresh(ctx, request.(PostAuthRefreshRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostAuthRefresh") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostAuthRefreshResponseObject); ok { + if err := validResponse.VisitPostAuthRefreshResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetHealthz operation middleware +func (sh *strictHandler) GetHealthz(w http.ResponseWriter, r *http.Request) { + var request GetHealthzRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetHealthz(ctx, request.(GetHealthzRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetHealthz") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetHealthzResponseObject); ok { + if err := validResponse.VisitGetHealthzResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// Base64 encoded, compressed with deflate, json marshaled OpenAPI spec. +// Stored as a slice of fixed-width chunks rather than one concatenated +// const string: with thousands of chunks the chained `+` fold is several +// times slower for the Go compiler than parsing a slice literal. +var swaggerSpec = []string{ + "zFjdb9s2EP9XCG4PG6raTtq9aNhDEnQfRVBkSYs9BIFxls4WY4pkyaNTr/D/PpBUbMmW4RSrg7xJ4n1/", + "/O6or7zQtdEKFTmef+WuqLCG+PjOWm3Dg7HaoCWB8TP2fy50ifFY+Zrnt1yoBUhRjguLJSoSIB3POBSF", + "9orGUhdzLHnGSc9RjfGLEbb13jDzjN+jlMupUGOHzgndJp1AMUdVjr2CBQgJE4k84xYIx1LUgiJRlAMU", + "OKcgZPwmFKFVIPldxmlpkOfckRVqxlcZL5FAyOiS8jJJzcl6XGW8RudgFt3c4ltl3OJnHy3Lb1MwNvQb", + "PXpyjwXxhhwdjUV5WFwKeYenT+Slngl1nWh2E1TiQhQ4ljBBuetdTyQMOPegbbSvFuoS1Ywqnp/0kHoX", + "AlrjQdItx9Z8LW19nv3tRTG/0EphQVdayr1OGi3lONbQ4ai2aA/pvCGwdI3OaOVwf+3vhvAbzGlq5oBV", + "1zi16Kq9AbDp/KlKu+R9Cj+GkysQPS0PRYHO7VWVHTQmFU44+NHilOf8h+EGjYYNFA0/BZptwzu6tzU1", + "cvvc+dQo3OoN4YyE5fixhnfsxBqE7D15Sv9GJEsisq6uXRNXGXdYeCtoeRMCkAw8R7Bo3//zMbxM4svv", + "2tZAPOfha5aAOwhKp3wtuCIyfBXkCjXVCQlcYYUJoMhzfjabWZwBCTVjDaSyqbbshvQMFBIrpEBFDIxx", + "A3bhrQ1vztspFMh+Y+CpYq9YhSCp+ncQFAsKsMLXEsCI18bqL8sAyGhdUjwanAxGIYTaoAIjeM7fxE8B", + "DKiKfg+D9KEMwJbaO9V8yF0E9b9KnvMr7ejMUxXxbwOT57pcpu5UhCrygTFSFJFzeO+02ky8Q0XYwdZV", + "N8HNdLANPkTDT0ej76Z704JR8Vb6PFVhvhYQBt4q429HJ99Nc1oBerSeQ8nakz3oPX1zfL1naX9gaX9g", + "pUdGmlk0GNxnYcR7i9GeX0bPYM/7Zj1h7SUk9rCva7BLnkfECb3OXrHHGcdiQbOfQk8ILBlVVvtZxdbS", + "2kk9X36AGn+OUtftoD09qR8C3XEaYmsQPakl3u6izzUudNgE27jH89sO4t3ere7aEU08DFiD+iyh/naA", + "hiDlU4N0JiU/trlSdg12EWapQuYfJ1wy/3PYPF4XafUYhoXgsBvbG9KRsr5vEXtRiGiM1Qssf01hZgaE", + "ZcI5n/DxdHS6m9cbElKyBxDUjPW3J6Pjo8e7dJdh2jKLYfw/Vta6dkKMWQw6a6Ie5rDVC5B7C8aFZfXb", + "Kibut/yICdu/TPeE5UKX2ErYSwHycwygDRuU7ualAlW6CubYSkzT74eT0aDpC8LqZ2rWD/jQatNn21+u", + "20DMmt8NbMgseofh4fE3Q7cEumxWUzQqJbxZgINJM+xJ9R9IfzYk/zPY3buLIyDv2r9e9Lznz8bWlaTh", + "6rl/7MTqyupw12IgxWK7Jy7FAlU4NFZPmmZ2aMOSH8eit7K5grh8OAQjBq65FAwKXfPV3eq/AAAA//8=", +} + +// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, +// after base64-decoding and flate-decompressing the embedded blob. +func decodeSpec() ([]byte, error) { + encoded := strings.Join(swaggerSpec, "") + compressed, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr := flate.NewReader(bytes.NewReader(compressed)) + var buf bytes.Buffer + if _, err := buf.ReadFrom(zr); err != nil { + return nil, fmt.Errorf("read flate: %w", err) + } + if err := zr.Close(); err != nil { + return nil, fmt.Errorf("close flate reader: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cache of the decoded OpenAPI spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSpec returns the OpenAPI specification corresponding to the generated +// code in this file. External references in the spec are resolved through +// PathToRawSpec; externally-referenced files must be embedded in their +// corresponding Go packages (via the import-mapping feature). URL-based +// external refs are not supported. +func GetSpec() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} + +// GetSpecJSON returns the raw JSON bytes of the embedded OpenAPI +// specification: decompressed but not unmarshaled. External references +// are not resolved here; the bytes are the spec exactly as embedded by +// codegen. The result is cached at package init time, so repeated calls +// are cheap. +func GetSpecJSON() ([]byte, error) { + return rawSpec() +} + +// GetSwagger returns the OpenAPI specification corresponding to the +// generated code in this file. +// +// Deprecated: GetSwagger predates kin-openapi renaming openapi3.Swagger +// to openapi3.T. Use [GetSpec] instead. This wrapper is retained for +// backwards compatibility. +func GetSwagger() (*openapi3.T, error) { + return GetSpec() +} diff --git a/oapi-codegen.yaml b/oapi-codegen.yaml index 3746715..37a1da2 100644 --- a/oapi-codegen.yaml +++ b/oapi-codegen.yaml @@ -2,6 +2,7 @@ package: gen output: internal/gen/api.gen.go generate: std-http-server: true + strict-server: true models: true embedded-spec: true output-options: