From d40f8fc37558609e0a967c1347b54758953df297 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Sun, 15 Mar 2026 20:24:29 -0400 Subject: [PATCH] wip 2 --- api/v1/activity.go | 65 +++ api/v1/api.gen.go | 1103 ++++++++++++++++++++++++++++++++++++++ api/v1/auth.go | 227 ++++---- api/v1/auth_test.go | 222 ++++---- api/v1/documents.go | 141 +++-- api/v1/documents_test.go | 233 ++++---- api/v1/generate.go | 3 + api/v1/handlers.go | 294 ---------- api/v1/oapi-codegen.yaml | 6 + api/v1/openapi.yaml | 526 ++++++++++++++++++ api/v1/progress.go | 38 ++ api/v1/server.go | 48 +- api/v1/server_test.go | 92 ++-- api/v1/settings.go | 26 + api/v1/types.go | 76 --- api/v1/utils.go | 22 +- api/v1/utils_test.go | 93 ++-- flake.lock | 6 +- go.mod | 16 +- go.sum | 319 +---------- 20 files changed, 2316 insertions(+), 1240 deletions(-) create mode 100644 api/v1/activity.go create mode 100644 api/v1/api.gen.go create mode 100644 api/v1/generate.go delete mode 100644 api/v1/handlers.go create mode 100644 api/v1/oapi-codegen.yaml create mode 100644 api/v1/openapi.yaml create mode 100644 api/v1/progress.go create mode 100644 api/v1/settings.go delete mode 100644 api/v1/types.go diff --git a/api/v1/activity.go b/api/v1/activity.go new file mode 100644 index 0000000..90b46ae --- /dev/null +++ b/api/v1/activity.go @@ -0,0 +1,65 @@ +package v1 + +import ( + "context" + "strconv" + "time" + + "reichard.io/antholume/database" +) + +// GET /activity +func (s *Server) GetActivity(ctx context.Context, request GetActivityRequestObject) (GetActivityResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetActivity401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + docFilter := false + if request.Params.DocFilter != nil { + docFilter = *request.Params.DocFilter + } + + documentID := "" + if request.Params.DocumentId != nil { + documentID = *request.Params.DocumentId + } + + offset := int64(0) + if request.Params.Offset != nil { + offset = *request.Params.Offset + } + + limit := int64(100) + if request.Params.Limit != nil { + limit = *request.Params.Limit + } + + activities, err := s.db.Queries.GetActivity(ctx, database.GetActivityParams{ + UserID: auth.UserName, + DocFilter: docFilter, + DocumentID: documentID, + Offset: offset, + Limit: limit, + }) + if err != nil { + return GetActivity500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + apiActivities := make([]Activity, len(activities)) + for i, a := range activities { + apiActivities[i] = Activity{ + ActivityType: a.DeviceID, + DocumentId: a.DocumentID, + Id: strconv.Itoa(i), + Timestamp: time.Now(), + UserId: auth.UserName, + } + } + + response := ActivityResponse{ + Activities: apiActivities, + User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, + } + return GetActivity200JSONResponse(response), nil +} diff --git a/api/v1/api.gen.go b/api/v1/api.gen.go new file mode 100644 index 0000000..a06eccd --- /dev/null +++ b/api/v1/api.gen.go @@ -0,0 +1,1103 @@ +//go:build go1.22 + +// Package v1 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.0 DO NOT EDIT. +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/oapi-codegen/runtime" + strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" +) + +const ( + BearerAuthScopes = "BearerAuth.Scopes" +) + +// Activity defines model for Activity. +type Activity struct { + ActivityType string `json:"activity_type"` + DocumentId string `json:"document_id"` + Id string `json:"id"` + Timestamp time.Time `json:"timestamp"` + UserId string `json:"user_id"` +} + +// ActivityResponse defines model for ActivityResponse. +type ActivityResponse struct { + Activities []Activity `json:"activities"` + User UserData `json:"user"` +} + +// Document defines model for Document. +type Document struct { + Author string `json:"author"` + CreatedAt time.Time `json:"created_at"` + Deleted bool `json:"deleted"` + Id string `json:"id"` + Title string `json:"title"` + UpdatedAt time.Time `json:"updated_at"` + Words *int64 `json:"words,omitempty"` +} + +// DocumentResponse defines model for DocumentResponse. +type DocumentResponse struct { + Document Document `json:"document"` + Progress *Progress `json:"progress,omitempty"` + User UserData `json:"user"` +} + +// DocumentsResponse defines model for DocumentsResponse. +type DocumentsResponse struct { + Documents []Document `json:"documents"` + Limit int64 `json:"limit"` + NextPage *int64 `json:"next_page,omitempty"` + Page int64 `json:"page"` + PreviousPage *int64 `json:"previous_page,omitempty"` + Search *string `json:"search,omitempty"` + Total int64 `json:"total"` + User UserData `json:"user"` + WordCounts []WordCount `json:"word_counts"` +} + +// ErrorResponse defines model for ErrorResponse. +type ErrorResponse struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// LoginRequest defines model for LoginRequest. +type LoginRequest struct { + Password string `json:"password"` + Username string `json:"username"` +} + +// LoginResponse defines model for LoginResponse. +type LoginResponse struct { + IsAdmin bool `json:"is_admin"` + Username string `json:"username"` +} + +// Progress defines model for Progress. +type Progress struct { + CreatedAt time.Time `json:"created_at"` + DeviceId string `json:"device_id"` + DocumentId string `json:"document_id"` + Percentage float64 `json:"percentage"` + Progress string `json:"progress"` + UserId string `json:"user_id"` +} + +// ProgressResponse defines model for ProgressResponse. +type ProgressResponse = Progress + +// Setting defines model for Setting. +type Setting struct { + Id string `json:"id"` + Key string `json:"key"` + UserId string `json:"user_id"` + Value string `json:"value"` +} + +// SettingsResponse defines model for SettingsResponse. +type SettingsResponse struct { + Settings []Setting `json:"settings"` + Timezone *string `json:"timezone,omitempty"` + User UserData `json:"user"` +} + +// UserData defines model for UserData. +type UserData struct { + IsAdmin bool `json:"is_admin"` + Username string `json:"username"` +} + +// WordCount defines model for WordCount. +type WordCount struct { + Count int64 `json:"count"` + DocumentId string `json:"document_id"` +} + +// GetActivityParams defines parameters for GetActivity. +type GetActivityParams struct { + DocFilter *bool `form:"doc_filter,omitempty" json:"doc_filter,omitempty"` + DocumentId *string `form:"document_id,omitempty" json:"document_id,omitempty"` + Offset *int64 `form:"offset,omitempty" json:"offset,omitempty"` + Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` +} + +// GetDocumentsParams defines parameters for GetDocuments. +type GetDocumentsParams struct { + Page *int64 `form:"page,omitempty" json:"page,omitempty"` + Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` + Search *string `form:"search,omitempty" json:"search,omitempty"` +} + +// LoginJSONRequestBody defines body for Login for application/json ContentType. +type LoginJSONRequestBody = LoginRequest + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Get activity data + // (GET /activity) + GetActivity(w http.ResponseWriter, r *http.Request, params GetActivityParams) + // User login + // (POST /auth/login) + Login(w http.ResponseWriter, r *http.Request) + // User logout + // (POST /auth/logout) + Logout(w http.ResponseWriter, r *http.Request) + // Get current user info + // (GET /auth/me) + GetMe(w http.ResponseWriter, r *http.Request) + // List documents + // (GET /documents) + GetDocuments(w http.ResponseWriter, r *http.Request, params GetDocumentsParams) + // Get a single document + // (GET /documents/{id}) + GetDocument(w http.ResponseWriter, r *http.Request, id string) + // Get document progress + // (GET /progress/{id}) + GetProgress(w http.ResponseWriter, r *http.Request, id string) + // Get user settings + // (GET /settings) + GetSettings(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 + +// GetActivity operation middleware +func (siw *ServerInterfaceWrapper) GetActivity(w http.ResponseWriter, r *http.Request) { + + var err error + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetActivityParams + + // ------------- Optional query parameter "doc_filter" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "doc_filter", r.URL.Query(), ¶ms.DocFilter, runtime.BindQueryParameterOptions{Type: "boolean", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "doc_filter", Err: err}) + return + } + + // ------------- Optional query parameter "document_id" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "document_id", r.URL.Query(), ¶ms.DocumentId, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "document_id", Err: err}) + return + } + + // ------------- Optional query parameter "offset" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetActivity(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// Login operation middleware +func (siw *ServerInterfaceWrapper) Login(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.Login(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// Logout operation middleware +func (siw *ServerInterfaceWrapper) Logout(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.Logout(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetMe operation middleware +func (siw *ServerInterfaceWrapper) GetMe(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetMe(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetDocuments operation middleware +func (siw *ServerInterfaceWrapper) GetDocuments(w http.ResponseWriter, r *http.Request) { + + var err error + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetDocumentsParams + + // ------------- Optional query parameter "page" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "page", r.URL.Query(), ¶ms.Page, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err}) + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + // ------------- Optional query parameter "search" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "search", r.URL.Query(), ¶ms.Search, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "search", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetDocuments(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetDocument operation middleware +func (siw *ServerInterfaceWrapper) GetDocument(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetDocument(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetProgress operation middleware +func (siw *ServerInterfaceWrapper) GetProgress(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "id" ------------- + var id string + + err = runtime.BindStyledParameterWithOptions("simple", "id", r.PathValue("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetProgress(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetSettings operation middleware +func (siw *ServerInterfaceWrapper) GetSettings(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetSettings(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)) + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +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("GET "+options.BaseURL+"/activity", wrapper.GetActivity) + m.HandleFunc("POST "+options.BaseURL+"/auth/login", wrapper.Login) + m.HandleFunc("POST "+options.BaseURL+"/auth/logout", wrapper.Logout) + m.HandleFunc("GET "+options.BaseURL+"/auth/me", wrapper.GetMe) + m.HandleFunc("GET "+options.BaseURL+"/documents", wrapper.GetDocuments) + m.HandleFunc("GET "+options.BaseURL+"/documents/{id}", wrapper.GetDocument) + m.HandleFunc("GET "+options.BaseURL+"/progress/{id}", wrapper.GetProgress) + m.HandleFunc("GET "+options.BaseURL+"/settings", wrapper.GetSettings) + + return m +} + +type GetActivityRequestObject struct { + Params GetActivityParams +} + +type GetActivityResponseObject interface { + VisitGetActivityResponse(w http.ResponseWriter) error +} + +type GetActivity200JSONResponse ActivityResponse + +func (response GetActivity200JSONResponse) VisitGetActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetActivity401JSONResponse ErrorResponse + +func (response GetActivity401JSONResponse) VisitGetActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetActivity500JSONResponse ErrorResponse + +func (response GetActivity500JSONResponse) VisitGetActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type LoginRequestObject struct { + Body *LoginJSONRequestBody +} + +type LoginResponseObject interface { + VisitLoginResponse(w http.ResponseWriter) error +} + +type Login200JSONResponse LoginResponse + +func (response Login200JSONResponse) VisitLoginResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type Login400JSONResponse ErrorResponse + +func (response Login400JSONResponse) VisitLoginResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type Login401JSONResponse ErrorResponse + +func (response Login401JSONResponse) VisitLoginResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type Login500JSONResponse ErrorResponse + +func (response Login500JSONResponse) VisitLoginResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type LogoutRequestObject struct { +} + +type LogoutResponseObject interface { + VisitLogoutResponse(w http.ResponseWriter) error +} + +type Logout200Response struct { +} + +func (response Logout200Response) VisitLogoutResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type Logout401JSONResponse ErrorResponse + +func (response Logout401JSONResponse) VisitLogoutResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetMeRequestObject struct { +} + +type GetMeResponseObject interface { + VisitGetMeResponse(w http.ResponseWriter) error +} + +type GetMe200JSONResponse LoginResponse + +func (response GetMe200JSONResponse) VisitGetMeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetMe401JSONResponse ErrorResponse + +func (response GetMe401JSONResponse) VisitGetMeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocumentsRequestObject struct { + Params GetDocumentsParams +} + +type GetDocumentsResponseObject interface { + VisitGetDocumentsResponse(w http.ResponseWriter) error +} + +type GetDocuments200JSONResponse DocumentsResponse + +func (response GetDocuments200JSONResponse) VisitGetDocumentsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocuments401JSONResponse ErrorResponse + +func (response GetDocuments401JSONResponse) VisitGetDocumentsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocuments500JSONResponse ErrorResponse + +func (response GetDocuments500JSONResponse) VisitGetDocumentsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocumentRequestObject struct { + Id string `json:"id"` +} + +type GetDocumentResponseObject interface { + VisitGetDocumentResponse(w http.ResponseWriter) error +} + +type GetDocument200JSONResponse DocumentResponse + +func (response GetDocument200JSONResponse) VisitGetDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocument401JSONResponse ErrorResponse + +func (response GetDocument401JSONResponse) VisitGetDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocument404JSONResponse ErrorResponse + +func (response GetDocument404JSONResponse) VisitGetDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocument500JSONResponse ErrorResponse + +func (response GetDocument500JSONResponse) VisitGetDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetProgressRequestObject struct { + Id string `json:"id"` +} + +type GetProgressResponseObject interface { + VisitGetProgressResponse(w http.ResponseWriter) error +} + +type GetProgress200JSONResponse ProgressResponse + +func (response GetProgress200JSONResponse) VisitGetProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetProgress401JSONResponse ErrorResponse + +func (response GetProgress401JSONResponse) VisitGetProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetProgress404JSONResponse ErrorResponse + +func (response GetProgress404JSONResponse) VisitGetProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetProgress500JSONResponse ErrorResponse + +func (response GetProgress500JSONResponse) VisitGetProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetSettingsRequestObject struct { +} + +type GetSettingsResponseObject interface { + VisitGetSettingsResponse(w http.ResponseWriter) error +} + +type GetSettings200JSONResponse SettingsResponse + +func (response GetSettings200JSONResponse) VisitGetSettingsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetSettings401JSONResponse ErrorResponse + +func (response GetSettings401JSONResponse) VisitGetSettingsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetSettings500JSONResponse ErrorResponse + +func (response GetSettings500JSONResponse) VisitGetSettingsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // Get activity data + // (GET /activity) + GetActivity(ctx context.Context, request GetActivityRequestObject) (GetActivityResponseObject, error) + // User login + // (POST /auth/login) + Login(ctx context.Context, request LoginRequestObject) (LoginResponseObject, error) + // User logout + // (POST /auth/logout) + Logout(ctx context.Context, request LogoutRequestObject) (LogoutResponseObject, error) + // Get current user info + // (GET /auth/me) + GetMe(ctx context.Context, request GetMeRequestObject) (GetMeResponseObject, error) + // List documents + // (GET /documents) + GetDocuments(ctx context.Context, request GetDocumentsRequestObject) (GetDocumentsResponseObject, error) + // Get a single document + // (GET /documents/{id}) + GetDocument(ctx context.Context, request GetDocumentRequestObject) (GetDocumentResponseObject, error) + // Get document progress + // (GET /progress/{id}) + GetProgress(ctx context.Context, request GetProgressRequestObject) (GetProgressResponseObject, error) + // Get user settings + // (GET /settings) + GetSettings(ctx context.Context, request GetSettingsRequestObject) (GetSettingsResponseObject, error) +} + +type StrictHandlerFunc = strictnethttp.StrictHTTPHandlerFunc +type StrictMiddlewareFunc = strictnethttp.StrictHTTPMiddlewareFunc + +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 +} + +// GetActivity operation middleware +func (sh *strictHandler) GetActivity(w http.ResponseWriter, r *http.Request, params GetActivityParams) { + var request GetActivityRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetActivity(ctx, request.(GetActivityRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetActivity") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetActivityResponseObject); ok { + if err := validResponse.VisitGetActivityResponse(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)) + } +} + +// Login operation middleware +func (sh *strictHandler) Login(w http.ResponseWriter, r *http.Request) { + var request LoginRequestObject + + var body LoginJSONRequestBody + 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.Login(ctx, request.(LoginRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "Login") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(LoginResponseObject); ok { + if err := validResponse.VisitLoginResponse(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)) + } +} + +// Logout operation middleware +func (sh *strictHandler) Logout(w http.ResponseWriter, r *http.Request) { + var request LogoutRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.Logout(ctx, request.(LogoutRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "Logout") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(LogoutResponseObject); ok { + if err := validResponse.VisitLogoutResponse(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)) + } +} + +// GetMe operation middleware +func (sh *strictHandler) GetMe(w http.ResponseWriter, r *http.Request) { + var request GetMeRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetMe(ctx, request.(GetMeRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetMe") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetMeResponseObject); ok { + if err := validResponse.VisitGetMeResponse(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)) + } +} + +// GetDocuments operation middleware +func (sh *strictHandler) GetDocuments(w http.ResponseWriter, r *http.Request, params GetDocumentsParams) { + var request GetDocumentsRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDocuments(ctx, request.(GetDocumentsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDocuments") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDocumentsResponseObject); ok { + if err := validResponse.VisitGetDocumentsResponse(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)) + } +} + +// GetDocument operation middleware +func (sh *strictHandler) GetDocument(w http.ResponseWriter, r *http.Request, id string) { + var request GetDocumentRequestObject + + request.Id = id + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDocument(ctx, request.(GetDocumentRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDocument") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDocumentResponseObject); ok { + if err := validResponse.VisitGetDocumentResponse(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)) + } +} + +// GetProgress operation middleware +func (sh *strictHandler) GetProgress(w http.ResponseWriter, r *http.Request, id string) { + var request GetProgressRequestObject + + request.Id = id + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetProgress(ctx, request.(GetProgressRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetProgress") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetProgressResponseObject); ok { + if err := validResponse.VisitGetProgressResponse(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)) + } +} + +// GetSettings operation middleware +func (sh *strictHandler) GetSettings(w http.ResponseWriter, r *http.Request) { + var request GetSettingsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetSettings(ctx, request.(GetSettingsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetSettings") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetSettingsResponseObject); ok { + if err := validResponse.VisitGetSettingsResponse(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)) + } +} diff --git a/api/v1/auth.go b/api/v1/auth.go index ee127f7..58df79d 100644 --- a/api/v1/auth.go +++ b/api/v1/auth.go @@ -3,7 +3,6 @@ package v1 import ( "context" "crypto/md5" - "encoding/json" "fmt" "net/http" "time" @@ -13,24 +12,125 @@ import ( log "github.com/sirupsen/logrus" ) -// authData represents session authentication data -type authData struct { - UserName string - IsAdmin bool - AuthHash string +// POST /auth/login +func (s *Server) Login(ctx context.Context, request LoginRequestObject) (LoginResponseObject, error) { + if request.Body == nil { + return Login400JSONResponse{Code: 400, Message: "Invalid request body"}, nil + } + + req := *request.Body + if req.Username == "" || req.Password == "" { + return Login400JSONResponse{Code: 400, Message: "Invalid credentials"}, nil + } + + // MD5 - KOSync compatibility + password := fmt.Sprintf("%x", md5.Sum([]byte(req.Password))) + + // Verify credentials + user, err := s.db.Queries.GetUser(ctx, req.Username) + if err != nil { + return Login401JSONResponse{Code: 401, Message: "Invalid credentials"}, nil + } + + if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || !match { + return Login401JSONResponse{Code: 401, Message: "Invalid credentials"}, nil + } + + // Get request and response from context (set by middleware) + r := s.getRequestFromContext(ctx) + w := s.getResponseWriterFromContext(ctx) + + if r == nil || w == nil { + return Login500JSONResponse{Code: 500, Message: "Internal context error"}, nil + } + + // Create session + store := sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey)) + if s.cfg.CookieEncKey != "" { + if len(s.cfg.CookieEncKey) == 16 || len(s.cfg.CookieEncKey) == 32 { + store = sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey), []byte(s.cfg.CookieEncKey)) + } + } + + session, _ := store.Get(r, "token") + session.Values["authorizedUser"] = user.ID + session.Values["isAdmin"] = user.Admin + session.Values["expiresAt"] = time.Now().Unix() + (60 * 60 * 24 * 7) + session.Values["authHash"] = *user.AuthHash + + if err := session.Save(r, w); err != nil { + return Login500JSONResponse{Code: 500, Message: "Failed to create session"}, nil + } + + return Login200JSONResponse{ + Username: user.ID, + IsAdmin: user.Admin, + }, nil } -// withAuth wraps a handler with session authentication -func (s *Server) withAuth(handler http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - auth, ok := s.getSession(r) - if !ok { - writeJSONError(w, http.StatusUnauthorized, "Unauthorized") - return - } - ctx := context.WithValue(r.Context(), "auth", auth) - handler(w, r.WithContext(ctx)) +// POST /auth/logout +func (s *Server) Logout(ctx context.Context, request LogoutRequestObject) (LogoutResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return Logout401JSONResponse{Code: 401, Message: "Unauthorized"}, nil } + + r := s.getRequestFromContext(ctx) + w := s.getResponseWriterFromContext(ctx) + + if r == nil || w == nil { + return Logout401JSONResponse{Code: 401, Message: "Internal context error"}, nil + } + + store := sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey)) + session, _ := store.Get(r, "token") + session.Values = make(map[any]any) + + if err := session.Save(r, w); err != nil { + return Logout401JSONResponse{Code: 401, Message: "Failed to logout"}, nil + } + + return Logout200Response{}, nil +} + +// GET /auth/me +func (s *Server) GetMe(ctx context.Context, request GetMeRequestObject) (GetMeResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetMe401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + return GetMe200JSONResponse{ + Username: auth.UserName, + IsAdmin: auth.IsAdmin, + }, nil +} + +// getSessionFromContext extracts authData from context +func (s *Server) getSessionFromContext(ctx context.Context) (authData, bool) { + auth, ok := ctx.Value("auth").(authData) + if !ok { + return authData{}, false + } + return auth, true +} + +// getRequestFromContext extracts the HTTP request from context +func (s *Server) getRequestFromContext(ctx context.Context) *http.Request { + r, ok := ctx.Value("request").(*http.Request) + if !ok { + return nil + } + return r +} + +// getResponseWriterFromContext extracts the response writer from context +func (s *Server) getResponseWriterFromContext(ctx context.Context) http.ResponseWriter { + w, ok := ctx.Value("response").(http.ResponseWriter) + if !ok { + return nil + } + return w } // getSession retrieves auth data from the session cookie @@ -86,94 +186,9 @@ func (s *Server) getUserAuthHash(ctx context.Context, username string) (string, return *user.AuthHash, nil } -// apiLogin handles POST /api/v1/auth/login -func (s *Server) apiLogin(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - writeJSONError(w, http.StatusMethodNotAllowed, "Method not allowed") - return - } - - var req LoginRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeJSONError(w, http.StatusBadRequest, "Invalid JSON") - return - } - - if req.Username == "" || req.Password == "" { - writeJSONError(w, http.StatusBadRequest, "Invalid credentials") - return - } - - // MD5 - KOSync compatibility - password := fmt.Sprintf("%x", md5.Sum([]byte(req.Password))) - - // Verify credentials - user, err := s.db.Queries.GetUser(r.Context(), req.Username) - if err != nil { - writeJSONError(w, http.StatusUnauthorized, "Invalid credentials") - return - } - - if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || !match { - writeJSONError(w, http.StatusUnauthorized, "Invalid credentials") - return - } - - // Create session - store := sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey)) - if s.cfg.CookieEncKey != "" { - if len(s.cfg.CookieEncKey) == 16 || len(s.cfg.CookieEncKey) == 32 { - store = sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey), []byte(s.cfg.CookieEncKey)) - } - } - - session, _ := store.Get(r, "token") - session.Values["authorizedUser"] = user.ID - session.Values["isAdmin"] = user.Admin - session.Values["expiresAt"] = time.Now().Unix() + (60 * 60 * 24 * 7) - session.Values["authHash"] = *user.AuthHash - - if err := session.Save(r, w); err != nil { - writeJSONError(w, http.StatusInternalServerError, "Failed to create session") - return - } - - writeJSON(w, http.StatusOK, LoginResponse{ - Username: user.ID, - IsAdmin: user.Admin, - }) +// authData represents authenticated user information +type authData struct { + UserName string + IsAdmin bool + AuthHash string } - -// apiLogout handles POST /api/v1/auth/logout -func (s *Server) apiLogout(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - writeJSONError(w, http.StatusMethodNotAllowed, "Method not allowed") - return - } - - store := sessions.NewCookieStore([]byte(s.cfg.CookieAuthKey)) - session, _ := store.Get(r, "token") - session.Values = make(map[any]any) - - if err := session.Save(r, w); err != nil { - writeJSONError(w, http.StatusInternalServerError, "Failed to logout") - return - } - - writeJSON(w, http.StatusOK, map[string]string{"status": "logged out"}) -} - -// apiGetMe handles GET /api/v1/auth/me -func (s *Server) apiGetMe(w http.ResponseWriter, r *http.Request) { - auth, ok := r.Context().Value("auth").(authData) - if !ok { - writeJSONError(w, http.StatusUnauthorized, "Unauthorized") - return - } - - writeJSON(w, http.StatusOK, UserData{ - Username: auth.UserName, - IsAdmin: auth.IsAdmin, - }) -} - diff --git a/api/v1/auth_test.go b/api/v1/auth_test.go index f41e50b..dbe30ca 100644 --- a/api/v1/auth_test.go +++ b/api/v1/auth_test.go @@ -9,19 +9,90 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/suite" + argon2 "github.com/alexedwards/argon2id" + "reichard.io/antholume/config" "reichard.io/antholume/database" ) -func TestAPILogin(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() - server := NewServer(db, cfg) +type AuthTestSuite struct { + suite.Suite + db *database.DBManager + cfg *config.Config + srv *Server +} - // First, create a user - createTestUser(t, db, "testuser", "testpass") +func (suite *AuthTestSuite) setupConfig() *config.Config { + return &config.Config{ + ListenPort: "8080", + DBType: "memory", + DBName: "test", + ConfigPath: "/tmp", + CookieAuthKey: "test-auth-key-32-bytes-long-enough", + CookieEncKey: "0123456789abcdef", + CookieSecure: false, + CookieHTTPOnly: true, + Version: "test", + DemoMode: false, + RegistrationEnabled: true, + } +} + +func TestAuth(t *testing.T) { + suite.Run(t, new(AuthTestSuite)) +} + +func (suite *AuthTestSuite) SetupTest() { + suite.cfg = suite.setupConfig() + suite.db = database.NewMgr(suite.cfg) + suite.srv = NewServer(suite.db, suite.cfg) +} + +func (suite *AuthTestSuite) createTestUser(username, password string) { + md5Hash := fmt.Sprintf("%x", md5.Sum([]byte(password))) + + hashedPassword, err := argon2.CreateHash(md5Hash, argon2.DefaultParams) + suite.Require().NoError(err) + + authHash := "test-auth-hash" + + _, err = suite.db.Queries.CreateUser(suite.T().Context(), database.CreateUserParams{ + ID: username, + Pass: &hashedPassword, + AuthHash: &authHash, + Admin: true, + }) + suite.Require().NoError(err) +} + +func (suite *AuthTestSuite) login(username, password string) *http.Cookie { + reqBody := LoginRequest{ + Username: username, + Password: password, + } + body, err := json.Marshal(reqBody) + suite.Require().NoError(err) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusOK, w.Code, "login should return 200") + + var resp LoginResponse + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + + cookies := w.Result().Cookies() + suite.Require().Len(cookies, 1, "should have session cookie") + + return cookies[0] +} + +func (suite *AuthTestSuite) TestAPILogin() { + suite.createTestUser("testuser", "testpass") - // Test login reqBody := LoginRequest{ Username: "testuser", Password: "testpass", @@ -31,27 +102,16 @@ func TestAPILogin(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) w := httptest.NewRecorder() - server.ServeHTTP(w, req) + suite.srv.ServeHTTP(w, req) - if w.Code != http.StatusOK { - t.Fatalf("Expected 200, got %d: %s", w.Code, w.Body.String()) - } + suite.Equal(http.StatusOK, w.Code) var resp LoginResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - if resp.Username != "testuser" { - t.Errorf("Expected username 'testuser', got '%s'", resp.Username) - } + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal("testuser", resp.Username) } -func TestAPILoginInvalidCredentials(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() - server := NewServer(db, cfg) - +func (suite *AuthTestSuite) TestAPILoginInvalidCredentials() { reqBody := LoginRequest{ Username: "testuser", Password: "wrongpass", @@ -61,124 +121,46 @@ func TestAPILoginInvalidCredentials(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) w := httptest.NewRecorder() - server.ServeHTTP(w, req) + suite.srv.ServeHTTP(w, req) - if w.Code != http.StatusUnauthorized { - t.Fatalf("Expected 401, got %d", w.Code) - } + suite.Equal(http.StatusUnauthorized, w.Code) } -func TestAPILogout(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() - server := NewServer(db, cfg) +func (suite *AuthTestSuite) TestAPILogout() { + suite.createTestUser("testuser", "testpass") + cookie := suite.login("testuser", "testpass") - // Create user and login - createTestUser(t, db, "testuser", "testpass") - - // Login first - reqBody := LoginRequest{Username: "testuser", Password: "testpass"} - body, _ := json.Marshal(reqBody) - loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) - loginResp := httptest.NewRecorder() - server.ServeHTTP(loginResp, loginReq) - - // Get session cookie - cookies := loginResp.Result().Cookies() - if len(cookies) == 0 { - t.Fatal("No session cookie returned") - } - - // Logout req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/logout", nil) - req.AddCookie(cookies[0]) + req.AddCookie(cookie) w := httptest.NewRecorder() - server.ServeHTTP(w, req) + suite.srv.ServeHTTP(w, req) - if w.Code != http.StatusOK { - t.Fatalf("Expected 200, got %d", w.Code) - } + suite.Equal(http.StatusOK, w.Code) } -func TestAPIGetMe(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() - server := NewServer(db, cfg) +func (suite *AuthTestSuite) TestAPIGetMe() { + suite.createTestUser("testuser", "testpass") + cookie := suite.login("testuser", "testpass") - // Create user and login - createTestUser(t, db, "testuser", "testpass") - - // Login first - reqBody := LoginRequest{Username: "testuser", Password: "testpass"} - body, _ := json.Marshal(reqBody) - loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) - loginResp := httptest.NewRecorder() - server.ServeHTTP(loginResp, loginReq) - - // Get session cookie - cookies := loginResp.Result().Cookies() - if len(cookies) == 0 { - t.Fatal("No session cookie returned") - } - - // Get me req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil) - req.AddCookie(cookies[0]) + req.AddCookie(cookie) w := httptest.NewRecorder() - server.ServeHTTP(w, req) + suite.srv.ServeHTTP(w, req) - if w.Code != http.StatusOK { - t.Fatalf("Expected 200, got %d", w.Code) - } + suite.Equal(http.StatusOK, w.Code) var resp UserData - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - if resp.Username != "testuser" { - t.Errorf("Expected username 'testuser', got '%s'", resp.Username) - } + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal("testuser", resp.Username) } -func TestAPIGetMeUnauthenticated(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() - server := NewServer(db, cfg) - +func (suite *AuthTestSuite) TestAPIGetMeUnauthenticated() { req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil) w := httptest.NewRecorder() - server.ServeHTTP(w, req) + suite.srv.ServeHTTP(w, req) - if w.Code != http.StatusUnauthorized { - t.Fatalf("Expected 401, got %d", w.Code) - } -} - -func createTestUser(t *testing.T, db *database.DBManager, username, password string) { - t.Helper() - - // MD5 hash for KOSync compatibility (matches existing system) - md5Hash := fmt.Sprintf("%x", md5.Sum([]byte(password))) - - // Then argon2 hash the MD5 - hashedPassword, err := argon2.CreateHash(md5Hash, argon2.DefaultParams) - if err != nil { - t.Fatalf("Failed to hash password: %v", err) - } - - authHash := "test-auth-hash" - - _, err = db.Queries.CreateUser(t.Context(), database.CreateUserParams{ - ID: username, - Pass: &hashedPassword, - AuthHash: &authHash, - Admin: true, - }) - if err != nil { - t.Fatalf("Failed to create user: %v", err) - } + suite.Equal(http.StatusUnauthorized, w.Code) } \ No newline at end of file diff --git a/api/v1/documents.go b/api/v1/documents.go index 6b386c2..cb6cde8 100644 --- a/api/v1/documents.go +++ b/api/v1/documents.go @@ -1,141 +1,130 @@ package v1 import ( - "net/http" - "strconv" - "strings" + "context" "reichard.io/antholume/database" - "reichard.io/antholume/pkg/ptr" ) -// apiGetDocuments handles GET /api/v1/documents -// Deprecated: Use GetDocuments with DocumentListRequest instead -func (s *Server) apiGetDocuments(w http.ResponseWriter, r *http.Request) { - // Parse query params - query := r.URL.Query() - page, _ := strconv.ParseInt(query.Get("page"), 10, 64) - if page == 0 { - page = 1 - } - limit, _ := strconv.ParseInt(query.Get("limit"), 10, 64) - if limit == 0 { - limit = 9 - } - search := query.Get("search") - - // Get auth from context - auth, ok := r.Context().Value("auth").(authData) +// GET /documents +func (s *Server) GetDocuments(ctx context.Context, request GetDocumentsRequestObject) (GetDocumentsResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) if !ok { - writeJSONError(w, http.StatusUnauthorized, "Unauthorized") - return + return GetDocuments401JSONResponse{Code: 401, Message: "Unauthorized"}, nil } - // Build query - var queryPtr *string - if search != "" { - queryPtr = ptr.Of("%" + search + "%") + page := int64(1) + if request.Params.Page != nil { + page = *request.Params.Page + } + + limit := int64(9) + if request.Params.Limit != nil { + limit = *request.Params.Limit + } + + search := "" + if request.Params.Search != nil { + search = "%" + *request.Params.Search + "%" } - // Query database rows, err := s.db.Queries.GetDocumentsWithStats( - r.Context(), + ctx, database.GetDocumentsWithStatsParams{ UserID: auth.UserName, - Query: queryPtr, - Deleted: ptr.Of(false), + Query: &search, + Deleted: ptrOf(false), Offset: (page - 1) * limit, Limit: limit, }, ) if err != nil { - writeJSONError(w, http.StatusInternalServerError, err.Error()) - return + return GetDocuments500JSONResponse{Code: 500, Message: err.Error()}, nil } - // Calculate pagination total := int64(len(rows)) var nextPage *int64 var previousPage *int64 if page*limit < total { - nextPage = ptr.Of(page + 1) + nextPage = ptrOf(page + 1) } if page > 1 { - previousPage = ptr.Of(page - 1) + previousPage = ptrOf(page - 1) } - // Get word counts + apiDocuments := make([]Document, len(rows)) wordCounts := make([]WordCount, 0, len(rows)) - for _, row := range rows { + for i, row := range rows { + apiDocuments[i] = Document{ + Id: row.ID, + Title: *row.Title, + Author: *row.Author, + Words: row.Words, + } if row.Words != nil { wordCounts = append(wordCounts, WordCount{ - DocumentID: row.ID, + DocumentId: row.ID, Count: *row.Words, }) } } - // Return response - writeJSON(w, http.StatusOK, DocumentsResponse{ - Documents: rows, + response := DocumentsResponse{ + Documents: apiDocuments, Total: total, Page: page, Limit: limit, NextPage: nextPage, PreviousPage: previousPage, - Search: ptr.Of(search), + Search: request.Params.Search, User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, WordCounts: wordCounts, - }) + } + return GetDocuments200JSONResponse(response), nil } -// apiGetDocument handles GET /api/v1/documents/:id -// Deprecated: Use GetDocument with DocumentRequest instead -func (s *Server) apiGetDocument(w http.ResponseWriter, r *http.Request) { - // Extract ID from URL path - path := strings.TrimPrefix(r.URL.Path, "/api/v1/documents/") - id := strings.TrimPrefix(path, "/") - - if id == "" { - writeJSONError(w, http.StatusBadRequest, "Document ID required") - return - } - - // Get auth from context - auth, ok := r.Context().Value("auth").(authData) +// GET /documents/{id} +func (s *Server) GetDocument(ctx context.Context, request GetDocumentRequestObject) (GetDocumentResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) if !ok { - writeJSONError(w, http.StatusUnauthorized, "Unauthorized") - return + return GetDocument401JSONResponse{Code: 401, Message: "Unauthorized"}, nil } - // Query database - doc, err := s.db.Queries.GetDocument(r.Context(), id) + doc, err := s.db.Queries.GetDocument(ctx, request.Id) if err != nil { - writeJSONError(w, http.StatusNotFound, "Document not found") - return + return GetDocument404JSONResponse{Code: 404, Message: "Document not found"}, nil } - // Get progress - progressRow, err := s.db.Queries.GetDocumentProgress(r.Context(), database.GetDocumentProgressParams{ + progressRow, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{ UserID: auth.UserName, - DocumentID: id, + DocumentID: request.Id, }) var progress *Progress if err == nil { progress = &Progress{ - UserID: progressRow.UserID, - DocumentID: progressRow.DocumentID, - DeviceID: progressRow.DeviceID, + UserId: progressRow.UserID, + DocumentId: progressRow.DocumentID, + DeviceId: progressRow.DeviceID, Percentage: progressRow.Percentage, Progress: progressRow.Progress, - CreatedAt: progressRow.CreatedAt, + CreatedAt: parseTime(progressRow.CreatedAt), } } - // Return response - writeJSON(w, http.StatusOK, DocumentResponse{ - Document: doc, + apiDoc := Document{ + Id: doc.ID, + Title: *doc.Title, + Author: *doc.Author, + CreatedAt: parseTime(doc.CreatedAt), + UpdatedAt: parseTime(doc.UpdatedAt), + Deleted: doc.Deleted, + Words: doc.Words, + } + + response := DocumentResponse{ + Document: apiDoc, User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, Progress: progress, - }) -} \ No newline at end of file + } + return GetDocument200JSONResponse(response), nil +} diff --git a/api/v1/documents_test.go b/api/v1/documents_test.go index fe40cef..71ff15e 100644 --- a/api/v1/documents_test.go +++ b/api/v1/documents_test.go @@ -2,163 +2,160 @@ package v1 import ( "bytes" + "crypto/md5" "encoding/json" + "fmt" "net/http" "net/http/httptest" "testing" + "github.com/stretchr/testify/suite" + + argon2 "github.com/alexedwards/argon2id" + "reichard.io/antholume/config" "reichard.io/antholume/database" "reichard.io/antholume/pkg/ptr" ) -func TestAPIGetDocuments(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() - server := NewServer(db, cfg) +type DocumentsTestSuite struct { + suite.Suite + db *database.DBManager + cfg *config.Config + srv *Server +} - // Create user and login - createTestUser(t, db, "testuser", "testpass") - - // Login first - reqBody := LoginRequest{Username: "testuser", Password: "testpass"} - body, _ := json.Marshal(reqBody) - loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) - loginResp := httptest.NewRecorder() - server.ServeHTTP(loginResp, loginReq) - - // Get session cookie - cookies := loginResp.Result().Cookies() - if len(cookies) == 0 { - t.Fatal("No session cookie returned") - } - - // Get documents - req := httptest.NewRequest(http.MethodGet, "/api/v1/documents?page=1&limit=9", nil) - req.AddCookie(cookies[0]) - w := httptest.NewRecorder() - - server.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("Expected 200, got %d: %s", w.Code, w.Body.String()) - } - - var resp DocumentsResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - if resp.Page != 1 { - t.Errorf("Expected page 1, got %d", resp.Page) - } - - if resp.Limit != 9 { - t.Errorf("Expected limit 9, got %d", resp.Limit) - } - - if resp.User.Username != "testuser" { - t.Errorf("Expected username 'testuser', got '%s'", resp.User.Username) +func (suite *DocumentsTestSuite) setupConfig() *config.Config { + return &config.Config{ + ListenPort: "8080", + DBType: "memory", + DBName: "test", + ConfigPath: "/tmp", + CookieAuthKey: "test-auth-key-32-bytes-long-enough", + CookieEncKey: "0123456789abcdef", + CookieSecure: false, + CookieHTTPOnly: true, + Version: "test", + DemoMode: false, + RegistrationEnabled: true, } } -func TestAPIGetDocumentsUnauthenticated(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() - server := NewServer(db, cfg) +func TestDocuments(t *testing.T) { + suite.Run(t, new(DocumentsTestSuite)) +} +func (suite *DocumentsTestSuite) SetupTest() { + suite.cfg = suite.setupConfig() + suite.db = database.NewMgr(suite.cfg) + suite.srv = NewServer(suite.db, suite.cfg) +} + +func (suite *DocumentsTestSuite) createTestUser(username, password string) { + suite.authTestSuiteHelper(username, password) +} + +func (suite *DocumentsTestSuite) login(username, password string) *http.Cookie { + return suite.authLoginHelper(username, password) +} + +func (suite *DocumentsTestSuite) authTestSuiteHelper(username, password string) { + // MD5 hash for KOSync compatibility (matches existing system) + md5Hash := fmt.Sprintf("%x", md5.Sum([]byte(password))) + + // Then argon2 hash the MD5 + hashedPassword, err := argon2.CreateHash(md5Hash, argon2.DefaultParams) + suite.Require().NoError(err) + + _, err = suite.db.Queries.CreateUser(suite.T().Context(), database.CreateUserParams{ + ID: username, + Pass: &hashedPassword, + AuthHash: ptr.Of("test-auth-hash"), + Admin: true, + }) + suite.Require().NoError(err) +} + +func (suite *DocumentsTestSuite) authLoginHelper(username, password string) *http.Cookie { + reqBody := LoginRequest{Username: username, Password: password} + body, err := json.Marshal(reqBody) + suite.Require().NoError(err) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) + w := httptest.NewRecorder() + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusOK, w.Code) + + cookies := w.Result().Cookies() + suite.Require().Len(cookies, 1) + + return cookies[0] +} + +func (suite *DocumentsTestSuite) TestAPIGetDocuments() { + suite.createTestUser("testuser", "testpass") + cookie := suite.login("testuser", "testpass") + + req := httptest.NewRequest(http.MethodGet, "/api/v1/documents?page=1&limit=9", nil) + req.AddCookie(cookie) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusOK, w.Code) + + var resp DocumentsResponse + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal(int64(1), resp.Page) + suite.Equal(int64(9), resp.Limit) + suite.Equal("testuser", resp.User.Username) +} + +func (suite *DocumentsTestSuite) TestAPIGetDocumentsUnauthenticated() { req := httptest.NewRequest(http.MethodGet, "/api/v1/documents", nil) w := httptest.NewRecorder() - server.ServeHTTP(w, req) + suite.srv.ServeHTTP(w, req) - if w.Code != http.StatusUnauthorized { - t.Fatalf("Expected 401, got %d", w.Code) - } + suite.Equal(http.StatusUnauthorized, w.Code) } -func TestAPIGetDocument(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() - server := NewServer(db, cfg) +func (suite *DocumentsTestSuite) TestAPIGetDocument() { + suite.createTestUser("testuser", "testpass") - // Create user - createTestUser(t, db, "testuser", "testpass") - - // Create a document using UpsertDocument docID := "test-doc-1" - _, err := db.Queries.UpsertDocument(t.Context(), database.UpsertDocumentParams{ + _, err := suite.db.Queries.UpsertDocument(suite.T().Context(), database.UpsertDocumentParams{ ID: docID, Title: ptr.Of("Test Document"), Author: ptr.Of("Test Author"), }) - if err != nil { - t.Fatalf("Failed to create document: %v", err) - } + suite.Require().NoError(err) - // Login - reqBody := LoginRequest{Username: "testuser", Password: "testpass"} - body, _ := json.Marshal(reqBody) - loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) - loginResp := httptest.NewRecorder() - server.ServeHTTP(loginResp, loginReq) + cookie := suite.login("testuser", "testpass") - cookies := loginResp.Result().Cookies() - if len(cookies) == 0 { - t.Fatal("No session cookie returned") - } - - // Get document req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/"+docID, nil) - req.AddCookie(cookies[0]) + req.AddCookie(cookie) w := httptest.NewRecorder() - server.ServeHTTP(w, req) + suite.srv.ServeHTTP(w, req) - if w.Code != http.StatusOK { - t.Fatalf("Expected 200, got %d: %s", w.Code, w.Body.String()) - } + suite.Equal(http.StatusOK, w.Code) var resp DocumentResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - if resp.Document.ID != docID { - t.Errorf("Expected document ID '%s', got '%s'", docID, resp.Document.ID) - } - - if *resp.Document.Title != "Test Document" { - t.Errorf("Expected title 'Test Document', got '%s'", *resp.Document.Title) - } + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal(docID, resp.Document.Id) + suite.Equal("Test Document", resp.Document.Title) } -func TestAPIGetDocumentNotFound(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() - server := NewServer(db, cfg) +func (suite *DocumentsTestSuite) TestAPIGetDocumentNotFound() { + suite.createTestUser("testuser", "testpass") + cookie := suite.login("testuser", "testpass") - // Create user and login - createTestUser(t, db, "testuser", "testpass") - - reqBody := LoginRequest{Username: "testuser", Password: "testpass"} - body, _ := json.Marshal(reqBody) - loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) - loginResp := httptest.NewRecorder() - server.ServeHTTP(loginResp, loginReq) - - cookies := loginResp.Result().Cookies() - if len(cookies) == 0 { - t.Fatal("No session cookie returned") - } - - // Get non-existent document req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/non-existent", nil) - req.AddCookie(cookies[0]) + req.AddCookie(cookie) w := httptest.NewRecorder() - server.ServeHTTP(w, req) + suite.srv.ServeHTTP(w, req) - if w.Code != http.StatusNotFound { - t.Fatalf("Expected 404, got %d", w.Code) - } + suite.Equal(http.StatusNotFound, w.Code) } \ No newline at end of file diff --git a/api/v1/generate.go b/api/v1/generate.go new file mode 100644 index 0000000..ed7f92e --- /dev/null +++ b/api/v1/generate.go @@ -0,0 +1,3 @@ +package v1 + +//go:generate oapi-codegen -config oapi-codegen.yaml openapi.yaml diff --git a/api/v1/handlers.go b/api/v1/handlers.go deleted file mode 100644 index d44d5e5..0000000 --- a/api/v1/handlers.go +++ /dev/null @@ -1,294 +0,0 @@ -package v1 - -import ( - "context" - "net/http" - "strconv" - "strings" - - "reichard.io/antholume/database" -) - -// DocumentRequest represents a request for a single document -type DocumentRequest struct { - ID string -} - -// DocumentListRequest represents a request for listing documents -type DocumentListRequest struct { - Page int64 - Limit int64 - Search *string -} - -// ProgressRequest represents a request for document progress -type ProgressRequest struct { - ID string -} - -// ActivityRequest represents a request for activity data -type ActivityRequest struct { - DocFilter bool - DocumentID string - Offset int64 - Limit int64 -} - -// SettingsRequest represents a request for settings data -type SettingsRequest struct{} - -// GetDocument handles GET /api/v1/documents/:id -func (s *Server) GetDocument(ctx context.Context, req DocumentRequest) (DocumentResponse, error) { - auth := getAuthFromContext(ctx) - if auth == nil { - return DocumentResponse{}, &apiError{status: http.StatusUnauthorized, message: "Unauthorized"} - } - - doc, err := s.db.Queries.GetDocument(ctx, req.ID) - if err != nil { - return DocumentResponse{}, &apiError{status: http.StatusNotFound, message: "Document not found"} - } - - progressRow, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{ - UserID: auth.UserName, - DocumentID: req.ID, - }) - var progress *Progress - if err == nil { - progress = &Progress{ - UserID: progressRow.UserID, - DocumentID: progressRow.DocumentID, - DeviceID: progressRow.DeviceID, - Percentage: progressRow.Percentage, - Progress: progressRow.Progress, - CreatedAt: progressRow.CreatedAt, - } - } - - return DocumentResponse{ - Document: doc, - User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, - Progress: progress, - }, nil -} - -// GetDocuments handles GET /api/v1/documents -func (s *Server) GetDocuments(ctx context.Context, req DocumentListRequest) (DocumentsResponse, error) { - auth := getAuthFromContext(ctx) - if auth == nil { - return DocumentsResponse{}, &apiError{status: http.StatusUnauthorized, message: "Unauthorized"} - } - - rows, err := s.db.Queries.GetDocumentsWithStats( - ctx, - database.GetDocumentsWithStatsParams{ - UserID: auth.UserName, - Query: req.Search, - Deleted: ptrOf(false), - Offset: (req.Page - 1) * req.Limit, - Limit: req.Limit, - }, - ) - if err != nil { - return DocumentsResponse{}, &apiError{status: http.StatusInternalServerError, message: err.Error()} - } - - total := int64(len(rows)) - var nextPage *int64 - var previousPage *int64 - if req.Page*req.Limit < total { - nextPage = ptrOf(req.Page + 1) - } - if req.Page > 1 { - previousPage = ptrOf(req.Page - 1) - } - - wordCounts := make([]WordCount, 0, len(rows)) - for _, row := range rows { - if row.Words != nil { - wordCounts = append(wordCounts, WordCount{ - DocumentID: row.ID, - Count: *row.Words, - }) - } - } - - return DocumentsResponse{ - Documents: rows, - Total: total, - Page: req.Page, - Limit: req.Limit, - NextPage: nextPage, - PreviousPage: previousPage, - Search: req.Search, - User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, - WordCounts: wordCounts, - }, nil -} - -// GetProgress handles GET /api/v1/progress/:id -func (s *Server) GetProgress(ctx context.Context, req ProgressRequest) (Progress, error) { - auth := getAuthFromContext(ctx) - if auth == nil { - return Progress{}, &apiError{status: http.StatusUnauthorized, message: "Unauthorized"} - } - - if req.ID == "" { - return Progress{}, &apiError{status: http.StatusBadRequest, message: "Document ID required"} - } - - progressRow, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{ - UserID: auth.UserName, - DocumentID: req.ID, - }) - if err != nil { - return Progress{}, &apiError{status: http.StatusNotFound, message: "Progress not found"} - } - - return Progress{ - UserID: progressRow.UserID, - DocumentID: progressRow.DocumentID, - DeviceID: progressRow.DeviceID, - Percentage: progressRow.Percentage, - Progress: progressRow.Progress, - CreatedAt: progressRow.CreatedAt, - }, nil -} - -// GetActivity handles GET /api/v1/activity -func (s *Server) GetActivity(ctx context.Context, req ActivityRequest) (ActivityResponse, error) { - auth := getAuthFromContext(ctx) - if auth == nil { - return ActivityResponse{}, &apiError{status: http.StatusUnauthorized, message: "Unauthorized"} - } - - activities, err := s.db.Queries.GetActivity(ctx, database.GetActivityParams{ - UserID: auth.UserName, - DocFilter: req.DocFilter, - DocumentID: req.DocumentID, - Offset: req.Offset, - Limit: req.Limit, - }) - if err != nil { - return ActivityResponse{}, &apiError{status: http.StatusInternalServerError, message: err.Error()} - } - - return ActivityResponse{ - Activities: activities, - User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, - }, nil -} - -// GetSettings handles GET /api/v1/settings -func (s *Server) GetSettings(ctx context.Context, req SettingsRequest) (SettingsResponse, error) { - auth := getAuthFromContext(ctx) - if auth == nil { - return SettingsResponse{}, &apiError{status: http.StatusUnauthorized, message: "Unauthorized"} - } - - user, err := s.db.Queries.GetUser(ctx, auth.UserName) - if err != nil { - return SettingsResponse{}, &apiError{status: http.StatusInternalServerError, message: err.Error()} - } - - return SettingsResponse{ - Settings: []database.Setting{}, - User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, - Timezone: user.Timezone, - }, nil -} - -// getAuthFromContext extracts authData from context -func getAuthFromContext(ctx context.Context) *authData { - auth, ok := ctx.Value("auth").(authData) - if !ok { - return nil - } - return &auth -} - -// apiError represents an API error with status code -type apiError struct { - status int - message string -} - -// Error implements error interface -func (e *apiError) Error() string { - return e.message -} - -// handlerFunc is a generic API handler function -type handlerFunc[T, R any] func(context.Context, T) (R, error) - -// requestParser parses an HTTP request into a request struct -type requestParser[T any] func(*http.Request) T - -// handle wraps an API handler function with HTTP response writing -func handle[T, R any](fn handlerFunc[T, R], parser requestParser[T]) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - req := parser(r) - resp, err := fn(r.Context(), req) - if err != nil { - if apiErr, ok := err.(*apiError); ok { - writeJSONError(w, apiErr.status, apiErr.message) - } else { - writeJSONError(w, http.StatusInternalServerError, err.Error()) - } - return - } - writeJSON(w, http.StatusOK, resp) - } -} - -// parseDocumentRequest extracts document request from HTTP request -func parseDocumentRequest(r *http.Request) DocumentRequest { - path := strings.TrimPrefix(r.URL.Path, "/api/v1/documents/") - id := strings.TrimPrefix(path, "/") - return DocumentRequest{ID: id} -} - -// parseDocumentListRequest extracts document list request from URL query -func parseDocumentListRequest(r *http.Request) DocumentListRequest { - query := r.URL.Query() - page, _ := strconv.ParseInt(query.Get("page"), 10, 64) - if page == 0 { - page = 1 - } - limit, _ := strconv.ParseInt(query.Get("limit"), 10, 64) - if limit == 0 { - limit = 9 - } - search := query.Get("search") - var searchPtr *string - if search != "" { - searchPtr = ptrOf("%" + search + "%") - } - return DocumentListRequest{ - Page: page, - Limit: limit, - Search: searchPtr, - } -} - -// parseProgressRequest extracts progress request from HTTP request -func parseProgressRequest(r *http.Request) ProgressRequest { - path := strings.TrimPrefix(r.URL.Path, "/api/v1/progress/") - id := strings.TrimPrefix(path, "/") - return ProgressRequest{ID: id} -} - -// parseActivityRequest extracts activity request from HTTP request -func parseActivityRequest(r *http.Request) ActivityRequest { - return ActivityRequest{ - DocFilter: false, - DocumentID: "", - Offset: 0, - Limit: 100, - } -} - -// parseSettingsRequest extracts settings request from HTTP request -func parseSettingsRequest(r *http.Request) SettingsRequest { - return SettingsRequest{} -} \ No newline at end of file diff --git a/api/v1/oapi-codegen.yaml b/api/v1/oapi-codegen.yaml new file mode 100644 index 0000000..8ab8f4e --- /dev/null +++ b/api/v1/oapi-codegen.yaml @@ -0,0 +1,6 @@ +package: v1 +generate: + std-http-server: true + strict-server: true + models: true +output: api.gen.go diff --git a/api/v1/openapi.yaml b/api/v1/openapi.yaml new file mode 100644 index 0000000..91f1f2d --- /dev/null +++ b/api/v1/openapi.yaml @@ -0,0 +1,526 @@ +openapi: 3.0.3 +info: + title: AnthoLume API v1 + version: 1.0.0 + description: REST API for AnthoLume document management system + +servers: + - url: /api/v1 + +components: + schemas: + Document: + type: object + properties: + id: + type: string + title: + type: string + author: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + deleted: + type: boolean + words: + type: integer + format: int64 + required: + - id + - title + - author + - created_at + - updated_at + - deleted + + UserData: + type: object + properties: + username: + type: string + is_admin: + type: boolean + required: + - username + - is_admin + + WordCount: + type: object + properties: + document_id: + type: string + count: + type: integer + format: int64 + required: + - document_id + - count + + Progress: + type: object + properties: + user_id: + type: string + document_id: + type: string + device_id: + type: string + percentage: + type: number + format: double + progress: + type: string + created_at: + type: string + format: date-time + required: + - user_id + - document_id + - device_id + - percentage + - progress + - created_at + + Activity: + type: object + properties: + id: + type: string + user_id: + type: string + document_id: + type: string + activity_type: + type: string + timestamp: + type: string + format: date-time + required: + - id + - user_id + - document_id + - activity_type + - timestamp + + Setting: + type: object + properties: + id: + type: string + user_id: + type: string + key: + type: string + value: + type: string + required: + - id + - user_id + - key + - value + + DocumentsResponse: + type: object + properties: + documents: + type: array + items: + $ref: '#/components/schemas/Document' + total: + type: integer + format: int64 + page: + type: integer + format: int64 + limit: + type: integer + format: int64 + next_page: + type: integer + format: int64 + previous_page: + type: integer + format: int64 + search: + type: string + user: + $ref: '#/components/schemas/UserData' + word_counts: + type: array + items: + $ref: '#/components/schemas/WordCount' + required: + - documents + - total + - page + - limit + - user + - word_counts + + DocumentResponse: + type: object + properties: + document: + $ref: '#/components/schemas/Document' + user: + $ref: '#/components/schemas/UserData' + progress: + $ref: '#/components/schemas/Progress' + required: + - document + - user + + ProgressResponse: + $ref: '#/components/schemas/Progress' + + ActivityResponse: + type: object + properties: + activities: + type: array + items: + $ref: '#/components/schemas/Activity' + user: + $ref: '#/components/schemas/UserData' + required: + - activities + - user + + SettingsResponse: + type: object + properties: + settings: + type: array + items: + $ref: '#/components/schemas/Setting' + user: + $ref: '#/components/schemas/UserData' + timezone: + type: string + required: + - settings + - user + + LoginRequest: + type: object + properties: + username: + type: string + password: + type: string + required: + - username + - password + + LoginResponse: + type: object + properties: + username: + type: string + is_admin: + type: boolean + required: + - username + - is_admin + + ErrorResponse: + type: object + properties: + code: + type: integer + message: + type: string + required: + - code + - message + + securitySchemes: + BearerAuth: + type: http + scheme: bearer + +paths: + /documents: + get: + summary: List documents + operationId: getDocuments + tags: + - Documents + parameters: + - name: page + in: query + schema: + type: integer + format: int64 + default: 1 + - name: limit + in: query + schema: + type: integer + format: int64 + default: 9 + - name: search + in: query + schema: + type: string + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentsResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /documents/{id}: + get: + summary: Get a single document + operationId: getDocument + tags: + - Documents + parameters: + - name: id + in: path + required: true + schema: + type: string + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 404: + description: Document not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /progress/{id}: + get: + summary: Get document progress + operationId: getProgress + tags: + - Progress + parameters: + - name: id + in: path + required: true + schema: + type: string + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ProgressResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 404: + description: Progress not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /activity: + get: + summary: Get activity data + operationId: getActivity + tags: + - Activity + parameters: + - name: doc_filter + in: query + schema: + type: boolean + default: false + - name: document_id + in: query + schema: + type: string + - name: offset + in: query + schema: + type: integer + format: int64 + default: 0 + - name: limit + in: query + schema: + type: integer + format: int64 + default: 100 + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ActivityResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /settings: + get: + summary: Get user settings + operationId: getSettings + tags: + - Settings + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/SettingsResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /auth/login: + post: + summary: User login + operationId: login + tags: + - Auth + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + 200: + description: Successful login + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 401: + description: Invalid credentials + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /auth/logout: + post: + summary: User logout + operationId: logout + tags: + - Auth + security: + - BearerAuth: [] + responses: + 200: + description: Successful logout + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /auth/me: + get: + summary: Get current user info + operationId: getMe + tags: + - Auth + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' \ No newline at end of file diff --git a/api/v1/progress.go b/api/v1/progress.go new file mode 100644 index 0000000..907cec2 --- /dev/null +++ b/api/v1/progress.go @@ -0,0 +1,38 @@ +package v1 + +import ( + "context" + + "reichard.io/antholume/database" +) + +// GET /progress/{id} +func (s *Server) GetProgress(ctx context.Context, request GetProgressRequestObject) (GetProgressResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetProgress401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Id == "" { + return GetProgress404JSONResponse{Code: 404, Message: "Document ID required"}, nil + } + + progressRow, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{ + UserID: auth.UserName, + DocumentID: request.Id, + }) + if err != nil { + return GetProgress404JSONResponse{Code: 404, Message: "Progress not found"}, nil + } + + response := Progress{ + UserId: progressRow.UserID, + DocumentId: progressRow.DocumentID, + DeviceId: progressRow.DeviceID, + Percentage: progressRow.Percentage, + Progress: progressRow.Progress, + CreatedAt: parseTime(progressRow.CreatedAt), + } + return GetProgress200JSONResponse(response), nil +} + diff --git a/api/v1/server.go b/api/v1/server.go index 35e870f..d6fa5c6 100644 --- a/api/v1/server.go +++ b/api/v1/server.go @@ -1,12 +1,16 @@ package v1 import ( + "context" + "encoding/json" "net/http" "reichard.io/antholume/config" "reichard.io/antholume/database" ) +var _ StrictServerInterface = (*Server)(nil) + type Server struct { mux *http.ServeMux db *database.DBManager @@ -20,7 +24,11 @@ func NewServer(db *database.DBManager, cfg *config.Config) *Server { db: db, cfg: cfg, } - s.registerRoutes() + + // Create strict handler with authentication middleware + strictHandler := NewStrictHandler(s, []StrictMiddlewareFunc{s.authMiddleware}) + + s.mux = HandlerFromMuxWithBaseURL(strictHandler, s.mux, "/api/v1").(*http.ServeMux) return s } @@ -28,23 +36,31 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) } -// registerRoutes sets up all API routes -func (s *Server) registerRoutes() { - // Documents endpoints - s.mux.HandleFunc("/api/v1/documents", s.withAuth(wrapRequest(s.GetDocuments, parseDocumentListRequest))) - s.mux.HandleFunc("/api/v1/documents/", s.withAuth(wrapRequest(s.GetDocument, parseDocumentRequest))) +// authMiddleware adds authentication context to requests +func (s *Server) authMiddleware(handler StrictHandlerFunc, operationID string) StrictHandlerFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) { + // Store request and response in context for all handlers + ctx = context.WithValue(ctx, "request", r) + ctx = context.WithValue(ctx, "response", w) - // Progress endpoints - s.mux.HandleFunc("/api/v1/progress/", s.withAuth(wrapRequest(s.GetProgress, parseProgressRequest))) + // Skip auth for login endpoint + if operationID == "Login" { + return handler(ctx, w, r, request) + } - // Activity endpoints - s.mux.HandleFunc("/api/v1/activity", s.withAuth(wrapRequest(s.GetActivity, parseActivityRequest))) + auth, ok := s.getSession(r) + if !ok { + // Write 401 response directly + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + json.NewEncoder(w).Encode(ErrorResponse{Code: 401, Message: "Unauthorized"}) + return nil, nil + } - // Settings endpoints - s.mux.HandleFunc("/api/v1/settings", s.withAuth(wrapRequest(s.GetSettings, parseSettingsRequest))) + // Store auth in context for handlers to access + ctx = context.WithValue(ctx, "auth", auth) - // Auth endpoints - s.mux.HandleFunc("/api/v1/auth/login", s.apiLogin) - s.mux.HandleFunc("/api/v1/auth/logout", s.withAuth(s.apiLogout)) - s.mux.HandleFunc("/api/v1/auth/me", s.withAuth(s.apiGetMe)) + return handler(ctx, w, r, request) + } } + diff --git a/api/v1/server_test.go b/api/v1/server_test.go index d1d357c..93682a8 100644 --- a/api/v1/server_test.go +++ b/api/v1/server_test.go @@ -5,70 +5,54 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/suite" + "reichard.io/antholume/config" "reichard.io/antholume/database" ) -func TestNewServer(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() - - server := NewServer(db, cfg) - - if server == nil { - t.Fatal("NewServer returned nil") - } - - if server.mux == nil { - t.Fatal("Server mux is nil") - } - - if server.db == nil { - t.Fatal("Server db is nil") - } - - if server.cfg == nil { - t.Fatal("Server cfg is nil") - } +type ServerTestSuite struct { + suite.Suite + db *database.DBManager + cfg *config.Config + srv *Server } -func TestServerServeHTTP(t *testing.T) { - db := setupTestDB(t) - cfg := testConfig() +func TestServer(t *testing.T) { + suite.Run(t, new(ServerTestSuite)) +} - server := NewServer(db, cfg) +func (suite *ServerTestSuite) SetupTest() { + suite.cfg = &config.Config{ + ListenPort: "8080", + DBType: "memory", + DBName: "test", + ConfigPath: "/tmp", + CookieAuthKey: "test-auth-key-32-bytes-long-enough", + CookieEncKey: "0123456789abcdef", + CookieSecure: false, + CookieHTTPOnly: true, + Version: "test", + DemoMode: false, + RegistrationEnabled: true, + } + suite.db = database.NewMgr(suite.cfg) + suite.srv = NewServer(suite.db, suite.cfg) +} + +func (suite *ServerTestSuite) TestNewServer() { + suite.NotNil(suite.srv) + suite.NotNil(suite.srv.mux) + suite.NotNil(suite.srv.db) + suite.NotNil(suite.srv.cfg) +} + +func (suite *ServerTestSuite) TestServerServeHTTP() { req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil) w := httptest.NewRecorder() - server.ServeHTTP(w, req) + suite.srv.ServeHTTP(w, req) - if w.Code != http.StatusUnauthorized { - t.Fatalf("Expected 401 for unauthenticated request, got %d", w.Code) - } -} - -func setupTestDB(t *testing.T) *database.DBManager { - t.Helper() - - cfg := testConfig() - cfg.DBType = "memory" - - return database.NewMgr(cfg) -} - -func testConfig() *config.Config { - return &config.Config{ - ListenPort: "8080", - DBType: "memory", - DBName: "test", - ConfigPath: "/tmp", - CookieAuthKey: "test-auth-key-32-bytes-long-enough", - CookieEncKey: "0123456789abcdef", // Exactly 16 bytes - CookieSecure: false, - CookieHTTPOnly: true, - Version: "test", - DemoMode: false, - RegistrationEnabled: true, - } + suite.Equal(http.StatusUnauthorized, w.Code) } \ No newline at end of file diff --git a/api/v1/settings.go b/api/v1/settings.go new file mode 100644 index 0000000..2cc0a45 --- /dev/null +++ b/api/v1/settings.go @@ -0,0 +1,26 @@ +package v1 + +import ( + "context" +) + +// GET /settings +func (s *Server) GetSettings(ctx context.Context, request GetSettingsRequestObject) (GetSettingsResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetSettings401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + user, err := s.db.Queries.GetUser(ctx, auth.UserName) + if err != nil { + return GetSettings500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + response := SettingsResponse{ + Settings: []Setting{}, + User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, + Timezone: user.Timezone, + } + return GetSettings200JSONResponse(response), nil +} + diff --git a/api/v1/types.go b/api/v1/types.go deleted file mode 100644 index e9d74d7..0000000 --- a/api/v1/types.go +++ /dev/null @@ -1,76 +0,0 @@ -package v1 - -import "reichard.io/antholume/database" - -// DocumentsResponse is the API response for document list endpoints -type DocumentsResponse struct { - Documents []database.GetDocumentsWithStatsRow `json:"documents"` - Total int64 `json:"total"` - Page int64 `json:"page"` - Limit int64 `json:"limit"` - NextPage *int64 `json:"next_page"` - PreviousPage *int64 `json:"previous_page"` - Search *string `json:"search"` - User UserData `json:"user"` - WordCounts []WordCount `json:"word_counts"` -} - -// DocumentResponse is the API response for single document endpoints -type DocumentResponse struct { - Document database.Document `json:"document"` - User UserData `json:"user"` - Progress *Progress `json:"progress"` -} - -// UserData represents authenticated user context -type UserData struct { - Username string `json:"username"` - IsAdmin bool `json:"is_admin"` -} - -// WordCount represents computed word count statistics -type WordCount struct { - DocumentID string `json:"document_id"` - Count int64 `json:"count"` -} - -// Progress represents reading progress for a document -type Progress struct { - UserID string `json:"user_id"` - DocumentID string `json:"document_id"` - DeviceID string `json:"device_id"` - Percentage float64 `json:"percentage"` - Progress string `json:"progress"` - CreatedAt string `json:"created_at"` -} - -// ActivityResponse is the API response for activity endpoints -type ActivityResponse struct { - Activities []database.GetActivityRow `json:"activities"` - User UserData `json:"user"` -} - -// SettingsResponse is the API response for settings endpoints -type SettingsResponse struct { - Settings []database.Setting `json:"settings"` - User UserData `json:"user"` - Timezone *string `json:"timezone"` -} - -// LoginRequest is the request body for login -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -// LoginResponse is the response for successful login -type LoginResponse struct { - Username string `json:"username"` - IsAdmin bool `json:"is_admin"` -} - -// ErrorResponse represents an API error -type ErrorResponse struct { - Code int `json:"code"` - Message string `json:"message"` -} \ No newline at end of file diff --git a/api/v1/utils.go b/api/v1/utils.go index 1d818f0..1475e83 100644 --- a/api/v1/utils.go +++ b/api/v1/utils.go @@ -5,9 +5,10 @@ import ( "net/http" "net/url" "strconv" + "time" ) -// writeJSON writes a JSON response +// writeJSON writes a JSON response (deprecated - used by tests only) func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) @@ -16,7 +17,7 @@ func writeJSON(w http.ResponseWriter, status int, data any) { } } -// writeJSONError writes a JSON error response +// writeJSONError writes a JSON error response (deprecated - used by tests only) func writeJSONError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, ErrorResponse{ Code: status, @@ -24,14 +25,14 @@ func writeJSONError(w http.ResponseWriter, status int, message string) { }) } -// QueryParams represents parsed query parameters +// QueryParams represents parsed query parameters (deprecated - used by tests only) type QueryParams struct { - Page int64 - Limit int64 + Page int64 + Limit int64 Search *string } -// parseQueryParams parses URL query parameters +// parseQueryParams parses URL query parameters (deprecated - used by tests only) func parseQueryParams(query url.Values, defaultLimit int64) QueryParams { page, _ := strconv.ParseInt(query.Get("page"), 10, 64) if page == 0 { @@ -56,4 +57,13 @@ func parseQueryParams(query url.Values, defaultLimit int64) QueryParams { // ptrOf returns a pointer to the given value func ptrOf[T any](v T) *T { return &v +} + +// parseTime parses a string to time.Time +func parseTime(s string) time.Time { + t, _ := time.Parse(time.RFC3339, s) + if t.IsZero() { + t, _ = time.Parse("2006-01-02T15:04:05", s) + } + return t } \ No newline at end of file diff --git a/api/v1/utils_test.go b/api/v1/utils_test.go index de0e647..214176a 100644 --- a/api/v1/utils_test.go +++ b/api/v1/utils_test.go @@ -5,56 +5,46 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/suite" ) -func TestWriteJSON(t *testing.T) { +type UtilsTestSuite struct { + suite.Suite +} + +func TestUtils(t *testing.T) { + suite.Run(t, new(UtilsTestSuite)) +} + +func (suite *UtilsTestSuite) TestWriteJSON() { w := httptest.NewRecorder() data := map[string]string{"test": "value"} writeJSON(w, http.StatusOK, data) - if w.Header().Get("Content-Type") != "application/json" { - t.Errorf("Expected Content-Type 'application/json', got '%s'", w.Header().Get("Content-Type")) - } - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } + suite.Equal("application/json", w.Header().Get("Content-Type")) + suite.Equal(http.StatusOK, w.Code) var resp map[string]string - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - if resp["test"] != "value" { - t.Errorf("Expected 'value', got '%s'", resp["test"]) - } + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal("value", resp["test"]) } -func TestWriteJSONError(t *testing.T) { +func (suite *UtilsTestSuite) TestWriteJSONError() { w := httptest.NewRecorder() writeJSONError(w, http.StatusBadRequest, "test error") - if w.Code != http.StatusBadRequest { - t.Errorf("Expected status 400, got %d", w.Code) - } + suite.Equal(http.StatusBadRequest, w.Code) var resp ErrorResponse - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - if resp.Code != http.StatusBadRequest { - t.Errorf("Expected code 400, got %d", resp.Code) - } - - if resp.Message != "test error" { - t.Errorf("Expected message 'test error', got '%s'", resp.Message) - } + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal(http.StatusBadRequest, resp.Code) + suite.Equal("test error", resp.Message) } -func TestParseQueryParams(t *testing.T) { +func (suite *UtilsTestSuite) TestParseQueryParams() { query := make(map[string][]string) query["page"] = []string{"2"} query["limit"] = []string{"15"} @@ -62,46 +52,25 @@ func TestParseQueryParams(t *testing.T) { params := parseQueryParams(query, 9) - if params.Page != 2 { - t.Errorf("Expected page 2, got %d", params.Page) - } - - if params.Limit != 15 { - t.Errorf("Expected limit 15, got %d", params.Limit) - } - - if params.Search == nil { - t.Fatal("Expected search to be set") - } + suite.Equal(int64(2), params.Page) + suite.Equal(int64(15), params.Limit) + suite.NotNil(params.Search) } -func TestParseQueryParamsDefaults(t *testing.T) { +func (suite *UtilsTestSuite) TestParseQueryParamsDefaults() { query := make(map[string][]string) params := parseQueryParams(query, 9) - if params.Page != 1 { - t.Errorf("Expected page 1, got %d", params.Page) - } - - if params.Limit != 9 { - t.Errorf("Expected limit 9, got %d", params.Limit) - } - - if params.Search != nil { - t.Errorf("Expected search to be nil, got '%v'", params.Search) - } + suite.Equal(int64(1), params.Page) + suite.Equal(int64(9), params.Limit) + suite.Nil(params.Search) } -func TestPtrOf(t *testing.T) { +func (suite *UtilsTestSuite) TestPtrOf() { value := "test" ptr := ptrOf(value) - if ptr == nil { - t.Fatal("Expected non-nil pointer") - } - - if *ptr != "test" { - t.Errorf("Expected 'test', got '%s'", *ptr) - } + suite.NotNil(ptr) + suite.Equal("test", *ptr) } \ No newline at end of file diff --git a/flake.lock b/flake.lock index a82cd96..21ca9f2 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769089682, - "narHash": "sha256-9yA/LIuAVQq0lXelrZPjLuLVuZdm03p8tfmHhnDIkms=", + "lastModified": 1773524153, + "narHash": "sha256-Jms57zzlFf64ayKzzBWSE2SGvJmK+NGt8Gli71d9kmY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "078d69f03934859a181e81ba987c2bb033eebfc5", + "rev": "e9f278faa1d0c2fc835bd331d4666b59b505a410", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index b0b8fc5..b9b7851 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module reichard.io/antholume -go 1.24 +go 1.25 require ( github.com/PuerkitoBio/goquery v1.10.3 @@ -9,9 +9,11 @@ require ( github.com/gin-contrib/multitemplate v1.1.1 github.com/gin-contrib/sessions v1.0.4 github.com/gin-gonic/gin v1.10.1 + github.com/gorilla/sessions v1.4.0 github.com/itchyny/gojq v0.12.17 github.com/jarcoal/httpmock v1.3.1 github.com/microcosm-cc/bluemonday v1.0.27 + github.com/oapi-codegen/runtime v1.2.0 github.com/pkg/errors v0.9.1 github.com/pressly/goose/v3 v3.24.3 github.com/sirupsen/logrus v1.9.3 @@ -25,11 +27,10 @@ require ( require ( github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect - github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -43,10 +44,8 @@ require ( github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/gorilla/sessions v1.4.0 // indirect github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -65,7 +64,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/crypto v0.41.0 // indirect - golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect @@ -73,13 +71,7 @@ require ( golang.org/x/tools v0.36.0 // indirect google.golang.org/protobuf v1.36.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/uint128 v1.3.0 // indirect - modernc.org/cc/v3 v3.41.0 // indirect - modernc.org/ccgo/v3 v3.17.0 // indirect modernc.org/libc v1.66.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/opt v0.1.4 // indirect - modernc.org/strutil v1.2.1 // indirect - modernc.org/token v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index c583c09..49c91b7 100644 --- a/go.sum +++ b/go.sum @@ -1,147 +1,56 @@ -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0= -github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw= -github.com/ClickHouse/ch-go v0.65.1 h1:SLuxmLl5Mjj44/XbINsK2HFvzqup0s6rwKLFH347ZhU= -github.com/ClickHouse/clickhouse-go/v2 v2.16.0 h1:rhMfnPewXPnY4Q4lQRGdYuTLRBRKJEIEYHtbUMrzmvI= -github.com/ClickHouse/clickhouse-go/v2 v2.16.0/go.mod h1:J7SPfIxwR+x4mQ+o8MLSe0oY50NNntEqCIjFe/T1VPM= -github.com/ClickHouse/clickhouse-go/v2 v2.34.0 h1:Y4rqkdrRHgExvC4o/NTbLdY5LFQ3LHS77/RNFxFX3Co= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= -github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w= github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw= -github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= -github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= -github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= -github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= -github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= -github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= -github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= -github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= -github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg= -github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn0jg4= -github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ= -github.com/elastic/go-sysinfo v1.15.3 h1:W+RnmhKFkqPTCRoFq2VCTmsT4p/fwpo+3gKNQsn1XU0= -github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= -github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= -github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81 h1:hQ/WeoPMTbN8NHk5i96dWy3D4uF7yCU+kORyWG+P4oU= -github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81/go.mod h1:XLLtIXoP9+9zGcEDc7gAGV3AksGPO+vzv4kXHMJSdU0= github.com/gin-contrib/multitemplate v1.1.1 h1:uzhT/ZWS9nBd1h6P+AaxWaVSVAJRAcKH4yafrBU8sPc= github.com/gin-contrib/multitemplate v1.1.1/go.mod h1:1Sa4984P8+x87U0cg5yWxK4jpbK1cXMYegUCZK6XT/M= -github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE= -github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= -github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= -github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI= -github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY= -github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= -github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= -github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= @@ -150,82 +59,29 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= -github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= -github.com/itchyny/gojq v0.12.14 h1:6k8vVtsrhQSYgSGg827AD+PVVaB1NLXEdX+dda2oZCc= -github.com/itchyny/gojq v0.12.14/go.mod h1:y1G7oO7XkcR1LPZO59KyoCRy08T3j9vDYRV0GgYSS+s= github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= -github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= -github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= -github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= -github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= -github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= -github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= -github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= -github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= -github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -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/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= -github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -233,154 +89,74 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= -github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/opencontainers/runc v1.1.10 h1:EaL5WeO9lv9wmS6SASjszOeQdSctvpbu0DdBQBizE40= -github.com/opencontainers/runc v1.1.10/go.mod h1:+/R6+KmDlh+hOO8NkjmgkG9Qzvypzk0yXxAPYYR65+M= -github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= -github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= -github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s= -github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= -github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4= +github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= -github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pressly/goose/v3 v3.17.0 h1:fT4CL3LRm4kfyLuPWzDFAoxjR5ZHjeJ6uQhibQtBaIs= -github.com/pressly/goose/v3 v3.17.0/go.mod h1:22aw7NpnCPlS86oqkO/+3+o9FuCaJg4ZVWRUO3oGzHQ= github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM= github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec= -github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115 h1:OEAIMYp5l9kJ2kT9UPL5QSUriKIIDhnLmpJTy69sltA= -github.com/taylorskalyo/goreader v0.0.0-20230626212555-e7f5644f8115/go.mod h1:AIVbkIe1G7fpFHiKOdxZnU5p9tFPYNTQyH3H5IrRkGw= github.com/taylorskalyo/goreader v1.0.1 h1:eS9SYiHai2aAHhm+YMGRTqrvNt2aoRMTd7p6ftm0crY= github.com/taylorskalyo/goreader v1.0.1/go.mod h1:JrUsWCgnk4C3P5Jsr7Pf2mFrMpsR0ls/0bjR5aorYTI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= -github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw= -github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd h1:dzWP1Lu+A40W883dK/Mr3xyDSM/2MggS8GtHT0qgAnE= -github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= -github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 h1:LY6cI8cP4B9rrpTleZk95+08kl2gF4rixG7+V/dwL6Q= -github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 h1:E0yUuuX7UmPxXm92+yQCjMveLFO3zfvYFIJVuAqsVRA= -github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2/go.mod h1:fjBLQ2TdQNl4bMjuWl9adoTGBypwUTPoGC+EqYqiIcU= -github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1 h1:ixAiqjj2S/dNuJqrz4AxSqgw2P5OBMXp68hB5nNriUk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc= -go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel/trace v1.20.0 h1:+yxVAPZPbQhbC3OfAkeIVTky6iTFpcr4SiY9om7mXSQ= -go.opentelemetry.io/otel/trace v1.20.0/go.mod h1:HJSK7F/hA5RlzpZ0zKDCHCDHm556LCDtKaAo6JmBFUU= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= -golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= @@ -390,7 +166,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= @@ -398,21 +173,15 @@ golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -422,7 +191,6 @@ golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXct golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= @@ -431,12 +199,10 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= @@ -447,83 +213,42 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= -howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= -lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= -lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= -modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= -modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0= -modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= -modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA= -modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= -modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= -modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/libc v1.40.7 h1:oeLS0G067ZqUu+v143Dqad0btMfKmNS7SuOsnkq0Ysg= -modernc.org/libc v1.40.7/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= +modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I= +modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.66.6 h1:RyQpwAhM/19nXD8y3iejM/AjmKwY2TjxZTlUWTsWw2U= modernc.org/libc v1.66.6/go.mod h1:j8z0EYAuumoMQ3+cWXtmw6m+LYn3qm8dcZDFtFTSq+M= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= -modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= -modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= -modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= -modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=