diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..329ca39 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,75 @@ +# AnthoLume Agent Guide + +## 1) Working Style + +- Keep changes targeted. +- Do not refactor broadly unless the task requires it. +- Validate only what is relevant to the change when practical. +- If a fix will require substantial refactoring or wide-reaching changes, stop and ask first. + +## 2) Hard Rules + +- Never edit generated files directly. +- Never write ad-hoc SQL. +- For Go error wrapping, use `fmt.Errorf("message: %w", err)`. +- Do not use `github.com/pkg/errors`. + +## 3) Generated Code + +### OpenAPI +Edit: +- `api/v1/openapi.yaml` + +Regenerate: +- `go generate ./api/v1/generate.go` +- `cd frontend && bun run generate:api` + +Notes: +- If you add response headers in `api/v1/openapi.yaml` (for example `Set-Cookie`), `oapi-codegen` will generate typed response header structs in `api/v1/api.gen.go`; update the handler response values to populate those headers explicitly. + +Examples of generated files: +- `api/v1/api.gen.go` +- `frontend/src/generated/**/*.ts` + +### SQLC +Edit: +- `database/query.sql` + +Regenerate: +- `sqlc generate` + +## 4) Backend / Assets + +### Common commands +- Dev server: `make dev` +- Direct dev run: `CONFIG_PATH=./data DATA_PATH=./data REGISTRATION_ENABLED=true go run main.go serve` +- Tests: `make tests` +- Tailwind asset build: `make build_tailwind` + +### Notes +- The Go server embeds `templates/*` and `assets/*`. +- Root Tailwind output is built to `assets/style.css`. +- Be mindful of whether a change affects the embedded server-rendered app, the React frontend, or both. +- SQLite timestamps are stored as RFC3339 strings (usually with a trailing `Z`); prefer `parseTime` / `parseTimePtr` instead of ad-hoc `time.Parse` layouts. + +## 5) Frontend + +For frontend-specific implementation notes and commands, also read: +- `frontend/AGENTS.md` + +## 6) Regeneration Summary + +- Go API: `go generate ./api/v1/generate.go` +- Frontend API client: `cd frontend && bun run generate:api` +- SQLC: `sqlc generate` + +## 7) Updating This File + +After completing a task, update this `AGENTS.md` if you learned something general that would help future agents. + +Rules for updates: +- Add only repository-wide guidance. +- Do not add one-off task history. +- Keep updates short, concrete, and organized. +- Place new guidance in the most relevant section. +- If the new information would help future agents avoid repeated mistakes, add it proactively. diff --git a/antholume b/antholume new file mode 100755 index 0000000..94febc5 Binary files /dev/null and b/antholume differ diff --git a/api/api.go b/api/api.go index 49f3697..054aeda 100644 --- a/api/api.go +++ b/api/api.go @@ -16,7 +16,6 @@ import ( "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/microcosm-cc/bluemonday" - "github.com/pkg/errors" log "github.com/sirupsen/logrus" "reichard.io/antholume/config" "reichard.io/antholume/database" @@ -114,6 +113,11 @@ func (api *API) Start() error { return api.httpServer.ListenAndServe() } +// Handler returns the underlying http.Handler for the Gin router +func (api *API) Handler() http.Handler { + return api.httpServer.Handler +} + func (api *API) Stop() error { // Stop server ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -293,7 +297,7 @@ func (api *API) loadTemplates( templateDirectory := fmt.Sprintf("templates/%ss", basePath) allFiles, err := fs.ReadDir(api.assets, templateDirectory) if err != nil { - return errors.Wrap(err, fmt.Sprintf("unable to read template dir: %s", templateDirectory)) + return fmt.Errorf("unable to read template dir %s: %w", templateDirectory, err) } // Generate Templates @@ -305,7 +309,7 @@ func (api *API) loadTemplates( // Read Template b, err := fs.ReadFile(api.assets, templatePath) if err != nil { - return errors.Wrap(err, fmt.Sprintf("unable to read template: %s", templateName)) + return fmt.Errorf("unable to read template %s: %w", templateName, err) } // Clone? (Pages - Don't Stomp) @@ -316,7 +320,7 @@ func (api *API) loadTemplates( // Parse Template baseTemplate, err = baseTemplate.New(templateName).Parse(string(b)) if err != nil { - return errors.Wrap(err, fmt.Sprintf("unable to parse template: %s", templateName)) + return fmt.Errorf("unable to parse template %s: %w", templateName, err) } allTemplates[templateName] = baseTemplate diff --git a/api/app-admin-routes.go b/api/app-admin-routes.go index d43645f..6da9529 100644 --- a/api/app-admin-routes.go +++ b/api/app-admin-routes.go @@ -22,7 +22,6 @@ import ( "github.com/gabriel-vasile/mimetype" "github.com/gin-gonic/gin" "github.com/itchyny/gojq" - "github.com/pkg/errors" log "github.com/sirupsen/logrus" "reichard.io/antholume/database" "reichard.io/antholume/metadata" @@ -722,7 +721,7 @@ func (api *API) createBackup(ctx context.Context, w io.Writer, directories []str // Vacuum DB _, err := api.db.DB.ExecContext(ctx, "VACUUM;") if err != nil { - return errors.Wrap(err, "Unable to vacuum database") + return fmt.Errorf("Unable to vacuum database: %w", err) } ar := zip.NewWriter(w) @@ -796,7 +795,7 @@ func (api *API) createBackup(ctx context.Context, w io.Writer, directories []str func (api *API) isLastAdmin(ctx context.Context, userID string) (bool, error) { allUsers, err := api.db.Queries.GetUsers(ctx) if err != nil { - return false, errors.Wrap(err, fmt.Sprintf("GetUsers DB Error: %v", err)) + return false, fmt.Errorf("GetUsers DB Error: %w", err) } hasAdmin := false @@ -873,7 +872,7 @@ func (api *API) updateUser(ctx context.Context, user string, rawPassword *string } else { user, err := api.db.Queries.GetUser(ctx, user) if err != nil { - return errors.Wrap(err, fmt.Sprintf("GetUser DB Error: %v", err)) + return fmt.Errorf("GetUser DB Error: %w", err) } updateParams.Admin = user.Admin } @@ -911,7 +910,7 @@ func (api *API) updateUser(ctx context.Context, user string, rawPassword *string // Update User _, err := api.db.Queries.UpdateUser(ctx, updateParams) if err != nil { - return errors.Wrap(err, fmt.Sprintf("UpdateUser DB Error: %v", err)) + return fmt.Errorf("UpdateUser DB Error: %w", err) } return nil @@ -943,7 +942,7 @@ func (api *API) deleteUser(ctx context.Context, user string) error { // Delete User _, err = api.db.Queries.DeleteUser(ctx, user) if err != nil { - return errors.Wrap(err, fmt.Sprintf("DeleteUser DB Error: %v", err)) + return fmt.Errorf("DeleteUser DB Error: %w", err) } return nil diff --git a/api/v1/activity.go b/api/v1/activity.go new file mode 100644 index 0000000..c9fac7a --- /dev/null +++ b/api/v1/activity.go @@ -0,0 +1,151 @@ +package v1 + +import ( + "context" + "time" + + log "github.com/sirupsen/logrus" + "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 { + // Convert StartTime from interface{} to string + startTimeStr := "" + if a.StartTime != nil { + if str, ok := a.StartTime.(string); ok { + startTimeStr = str + } + } + + apiActivities[i] = Activity{ + DocumentId: a.DocumentID, + DeviceId: a.DeviceID, + StartTime: startTimeStr, + Title: a.Title, + Author: a.Author, + Duration: a.Duration, + StartPercentage: float32(a.StartPercentage), + EndPercentage: float32(a.EndPercentage), + ReadPercentage: float32(a.ReadPercentage), + } + } + + response := ActivityResponse{ + Activities: apiActivities, + } + return GetActivity200JSONResponse(response), nil +} + +// POST /activity +func (s *Server) CreateActivity(ctx context.Context, request CreateActivityRequestObject) (CreateActivityResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return CreateActivity401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return CreateActivity400JSONResponse{Code: 400, Message: "Request body is required"}, nil + } + + tx, err := s.db.DB.Begin() + if err != nil { + log.Error("Transaction Begin DB Error:", err) + return CreateActivity500JSONResponse{Code: 500, Message: "Database error"}, nil + } + committed := false + defer func() { + if committed { + return + } + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Debug("Transaction Rollback DB Error:", rollbackErr) + } + }() + + qtx := s.db.Queries.WithTx(tx) + + allDocumentsMap := make(map[string]struct{}) + for _, item := range request.Body.Activity { + allDocumentsMap[item.DocumentId] = struct{}{} + } + + for documentID := range allDocumentsMap { + if _, err := qtx.UpsertDocument(ctx, database.UpsertDocumentParams{ID: documentID}); err != nil { + log.Error("UpsertDocument DB Error:", err) + return CreateActivity400JSONResponse{Code: 400, Message: "Invalid document"}, nil + } + } + + if _, err := qtx.UpsertDevice(ctx, database.UpsertDeviceParams{ + ID: request.Body.DeviceId, + UserID: auth.UserName, + DeviceName: request.Body.DeviceName, + LastSynced: time.Now().UTC().Format(time.RFC3339), + }); err != nil { + log.Error("UpsertDevice DB Error:", err) + return CreateActivity400JSONResponse{Code: 400, Message: "Invalid device"}, nil + } + + for _, item := range request.Body.Activity { + if _, err := qtx.AddActivity(ctx, database.AddActivityParams{ + UserID: auth.UserName, + DocumentID: item.DocumentId, + DeviceID: request.Body.DeviceId, + StartTime: time.Unix(item.StartTime, 0).UTC().Format(time.RFC3339), + Duration: item.Duration, + StartPercentage: float64(item.Page) / float64(item.Pages), + EndPercentage: float64(item.Page+1) / float64(item.Pages), + }); err != nil { + log.Error("AddActivity DB Error:", err) + return CreateActivity400JSONResponse{Code: 400, Message: "Invalid activity"}, nil + } + } + + if err := tx.Commit(); err != nil { + log.Error("Transaction Commit DB Error:", err) + return CreateActivity500JSONResponse{Code: 500, Message: "Database error"}, nil + } + committed = true + + response := CreateActivityResponse{Added: int64(len(request.Body.Activity))} + return CreateActivity200JSONResponse(response), nil +} diff --git a/api/v1/admin.go b/api/v1/admin.go new file mode 100644 index 0000000..f003117 --- /dev/null +++ b/api/v1/admin.go @@ -0,0 +1,1070 @@ +package v1 + +import ( + "archive/zip" + "bufio" + "context" + "crypto/md5" + "encoding/json" + "fmt" + "io" + "io/fs" + "mime/multipart" + "os" + "path/filepath" + "sort" + "strings" + "time" + + argon2 "github.com/alexedwards/argon2id" + "github.com/itchyny/gojq" + log "github.com/sirupsen/logrus" + "reichard.io/antholume/database" + "reichard.io/antholume/metadata" + "reichard.io/antholume/utils" +) + +// GET /admin +func (s *Server) GetAdmin(ctx context.Context, request GetAdminRequestObject) (GetAdminResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return GetAdmin401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + // Get documents count using existing SQLC query + documentsSize, err := s.db.Queries.GetDocumentsSize(ctx, nil) + if err != nil { + return GetAdmin401JSONResponse{Code: 500, Message: err.Error()}, nil + } + + // For other counts, we need to aggregate across all users + // Get all users first + users, err := s.db.Queries.GetUsers(ctx) + if err != nil { + return GetAdmin401JSONResponse{Code: 500, Message: err.Error()}, nil + } + + var activitySize, progressSize, devicesSize int64 + for _, user := range users { + // Get user's database info using existing SQLC query + dbInfo, err := s.db.Queries.GetDatabaseInfo(ctx, user.ID) + if err == nil { + activitySize += dbInfo.ActivitySize + progressSize += dbInfo.ProgressSize + devicesSize += dbInfo.DevicesSize + } + } + + response := GetAdmin200JSONResponse{ + DatabaseInfo: &DatabaseInfo{ + DocumentsSize: documentsSize, + ActivitySize: activitySize, + ProgressSize: progressSize, + DevicesSize: devicesSize, + }, + } + return response, nil +} + +// POST /admin +func (s *Server) PostAdminAction(ctx context.Context, request PostAdminActionRequestObject) (PostAdminActionResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return PostAdminAction401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return PostAdminAction400JSONResponse{Code: 400, Message: "Missing request body"}, nil + } + + // Read the multipart form in a streaming way to support large files + reader := request.Body + form, err := reader.ReadForm(32 << 20) // 32MB for non-file fields (files are not stored in memory) + if err != nil { + return PostAdminAction400JSONResponse{Code: 400, Message: "Unable to parse form"}, nil + } + + // Extract action from form + actionValues := form.Value["action"] + if len(actionValues) == 0 { + return PostAdminAction400JSONResponse{Code: 400, Message: "Missing action"}, nil + } + action := actionValues[0] + + // Handle different admin actions mirroring legacy appPerformAdminAction + switch action { + case "METADATA_MATCH": + // This is a TODO in the legacy code as well + go func() { + // TODO: Implement metadata matching logic + log.Info("Metadata match action triggered (not yet implemented)") + }() + return PostAdminAction200JSONResponse{ + Message: "Metadata match started", + }, nil + + case "CACHE_TABLES": + // Cache temp tables asynchronously, matching legacy implementation + go func() { + err := s.db.CacheTempTables(context.Background()) + if err != nil { + log.Error("Unable to cache temp tables: ", err) + } + }() + return PostAdminAction200JSONResponse{ + Message: "Cache tables operation started", + }, nil + + case "BACKUP": + return s.handleBackupAction(ctx, request, form) + + case "RESTORE": + return s.handleRestoreAction(ctx, request, form) + + default: + return PostAdminAction400JSONResponse{Code: 400, Message: "Invalid action"}, nil + } +} + +// handleBackupAction handles the backup action, mirroring legacy createBackup logic +func (s *Server) handleBackupAction(ctx context.Context, request PostAdminActionRequestObject, form *multipart.Form) (PostAdminActionResponseObject, error) { + // Extract backup_types from form + backupTypesValues := form.Value["backup_types"] + + // Create a pipe for streaming the backup + pr, pw := io.Pipe() + + go func() { + defer pw.Close() + var directories []string + for _, val := range backupTypesValues { + if val == "COVERS" { + directories = append(directories, "covers") + } else if val == "DOCUMENTS" { + directories = append(directories, "documents") + } + } + log.Info("Starting backup for directories: ", directories) + err := s.createBackup(ctx, pw, directories) + if err != nil { + log.Error("Backup failed: ", err) + } else { + log.Info("Backup completed successfully") + } + }() + + // Set Content-Length to 0 to enable chunked transfer encoding + // This allows streaming with unknown file size + return PostAdminAction200ApplicationoctetStreamResponse{ + Body: pr, + ContentLength: 0, + }, nil +} + +// handleRestoreAction handles the restore action, mirroring legacy processRestoreFile logic +func (s *Server) handleRestoreAction(ctx context.Context, request PostAdminActionRequestObject, form *multipart.Form) (PostAdminActionResponseObject, error) { + // Get the uploaded file from form + fileHeaders := form.File["restore_file"] + if len(fileHeaders) == 0 { + return PostAdminAction400JSONResponse{Code: 400, Message: "Missing restore file"}, nil + } + + file, err := fileHeaders[0].Open() + if err != nil { + return PostAdminAction400JSONResponse{Code: 400, Message: "Unable to open restore file"}, nil + } + defer file.Close() + + // Create temp file for the uploaded file + tempFile, err := os.CreateTemp("", "restore") + if err != nil { + log.Warn("Temp File Create Error: ", err) + return PostAdminAction500JSONResponse{Code: 500, Message: "Unable to create temp file"}, nil + } + defer os.Remove(tempFile.Name()) + defer tempFile.Close() + + // Save uploaded file to temp + if _, err = io.Copy(tempFile, file); err != nil { + log.Error("Unable to save uploaded file: ", err) + return PostAdminAction500JSONResponse{Code: 500, Message: "Unable to save file"}, nil + } + + // Get file info and validate ZIP + fileInfo, err := tempFile.Stat() + if err != nil { + log.Error("Unable to read temp file: ", err) + return PostAdminAction500JSONResponse{Code: 500, Message: "Unable to read file"}, nil + } + + zipReader, err := zip.NewReader(tempFile, fileInfo.Size()) + if err != nil { + log.Error("Unable to read zip: ", err) + return PostAdminAction500JSONResponse{Code: 500, Message: "Unable to read zip"}, nil + } + + // Validate ZIP contents (mirroring legacy logic) + hasDBFile := false + hasUnknownFile := false + for _, file := range zipReader.File { + fileName := strings.TrimPrefix(file.Name, "/") + if fileName == "antholume.db" { + hasDBFile = true + } else if !strings.HasPrefix(fileName, "covers/") && !strings.HasPrefix(fileName, "documents/") { + hasUnknownFile = true + } + } + + if !hasDBFile { + return PostAdminAction500JSONResponse{Code: 500, Message: "Invalid Restore ZIP - Missing DB"}, nil + } else if hasUnknownFile { + return PostAdminAction500JSONResponse{Code: 500, Message: "Invalid Restore ZIP - Invalid File(s)"}, nil + } + + // Create backup before restoring (mirroring legacy logic) + log.Info("Creating backup before restore...") + backupFilePath := filepath.Join(s.cfg.ConfigPath, fmt.Sprintf("backups/AnthoLumeBackup_%s.zip", time.Now().Format("20060102150405"))) + backupFile, err := os.Create(backupFilePath) + if err != nil { + log.Error("Unable to create backup file: ", err) + return PostAdminAction500JSONResponse{Code: 500, Message: "Unable to create backup file"}, nil + } + defer backupFile.Close() + + w := bufio.NewWriter(backupFile) + err = s.createBackup(ctx, w, []string{"covers", "documents"}) + if err != nil { + log.Error("Unable to save backup file: ", err) + return PostAdminAction500JSONResponse{Code: 500, Message: "Unable to save backup file"}, nil + } + + // Remove data (mirroring legacy removeData) + log.Info("Removing data...") + err = s.removeData() + if err != nil { + log.Error("Unable to delete data: ", err) + return PostAdminAction500JSONResponse{Code: 500, Message: "Unable to delete data"}, nil + } + + // Restore data (mirroring legacy restoreData) + log.Info("Restoring data...") + err = s.restoreData(zipReader) + if err != nil { + log.Error("Unable to restore data: ", err) + return PostAdminAction500JSONResponse{Code: 500, Message: "Unable to restore data"}, nil + } + + // Reload DB (mirroring legacy Reload) + log.Info("Reloading database...") + if err := s.db.Reload(ctx); err != nil { + log.Error("Unable to reload DB: ", err) + return PostAdminAction500JSONResponse{Code: 500, Message: "Unable to reload DB"}, nil + } + + // Rotate auth hashes (mirroring legacy rotateAllAuthHashes) + log.Info("Rotating auth hashes...") + if err := s.rotateAllAuthHashes(ctx); err != nil { + log.Error("Unable to rotate hashes: ", err) + return PostAdminAction500JSONResponse{Code: 500, Message: "Unable to rotate hashes"}, nil + } + + log.Info("Restore completed successfully") + return PostAdminAction200JSONResponse{ + Message: "Restore completed successfully", + }, nil +} + +// createBackup creates a backup ZIP archive, mirroring legacy createBackup +func (s *Server) createBackup(ctx context.Context, w io.Writer, directories []string) error { + // Vacuum DB + _, err := s.db.DB.ExecContext(ctx, "VACUUM;") + if err != nil { + return fmt.Errorf("Unable to vacuum database: %w", err) + } + + ar := zip.NewWriter(w) + + // Helper function to walk and archive files + exportWalker := func(currentPath string, f fs.DirEntry, err error) error { + if err != nil { + return err + } + if f.IsDir() { + return nil + } + + file, err := os.Open(currentPath) + if err != nil { + return err + } + defer file.Close() + + fileName := filepath.Base(currentPath) + folderName := filepath.Base(filepath.Dir(currentPath)) + + newF, err := ar.Create(filepath.Join(folderName, fileName)) + if err != nil { + return err + } + + _, err = io.Copy(newF, file) + return err + } + + // Copy Database File (mirroring legacy logic) + fileName := fmt.Sprintf("%s.db", s.cfg.DBName) + dbLocation := filepath.Join(s.cfg.ConfigPath, fileName) + + dbFile, err := os.Open(dbLocation) + if err != nil { + return err + } + defer dbFile.Close() + + newDbFile, err := ar.Create(fileName) + if err != nil { + return err + } + + _, err = io.Copy(newDbFile, dbFile) + if err != nil { + return err + } + + // Backup Covers & Documents (mirroring legacy logic) + for _, dir := range directories { + err = filepath.WalkDir(filepath.Join(s.cfg.DataPath, dir), exportWalker) + if err != nil { + return err + } + } + + // Close writer to flush all data before returning + ar.Close() + return nil +} + +// removeData removes all data files, mirroring legacy removeData +func (s *Server) removeData() error { + allPaths := []string{ + "covers", + "documents", + "antholume.db", + "antholume.db-wal", + "antholume.db-shm", + } + + for _, name := range allPaths { + fullPath := filepath.Join(s.cfg.DataPath, name) + err := os.RemoveAll(fullPath) + if err != nil { + return err + } + } + + return nil +} + +// restoreData restores data from ZIP archive, mirroring legacy restoreData +func (s *Server) restoreData(zipReader *zip.Reader) error { + // Ensure Directories + s.cfg.EnsureDirectories() + + // Restore Data + for _, file := range zipReader.File { + rc, err := file.Open() + if err != nil { + return err + } + defer rc.Close() + + destPath := filepath.Join(s.cfg.DataPath, file.Name) + destFile, err := os.Create(destPath) + if err != nil { + log.Errorf("error creating destination file: %v", err) + return err + } + defer destFile.Close() + + // Copy the contents from the zip file to the destination file. + if _, err := io.Copy(destFile, rc); err != nil { + log.Errorf("Error copying file contents: %v", err) + return err + } + } + + return nil +} + +// rotateAllAuthHashes rotates all user auth hashes, mirroring legacy rotateAllAuthHashes +func (s *Server) rotateAllAuthHashes(ctx context.Context) error { + users, err := s.db.Queries.GetUsers(ctx) + if err != nil { + return err + } + + for _, user := range users { + rawAuthHash, err := utils.GenerateToken(64) + if err != nil { + return err + } + authHash := fmt.Sprintf("%x", rawAuthHash) + + _, err = s.db.Queries.UpdateUser(ctx, database.UpdateUserParams{ + UserID: user.ID, + AuthHash: &authHash, + Admin: user.Admin, + }) + if err != nil { + return err + } + } + + return nil +} + +// GET /admin/users +func (s *Server) GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return GetUsers401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + // Get users from database + users, err := s.db.Queries.GetUsers(ctx) + if err != nil { + return GetUsers500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + apiUsers := make([]User, len(users)) + for i, user := range users { + apiUsers[i] = User{ + Id: user.ID, + Admin: user.Admin, + CreatedAt: parseTime(user.CreatedAt), + } + } + + response := GetUsers200JSONResponse{ + Users: &apiUsers, + } + return response, nil +} + +// POST /admin/users +func (s *Server) UpdateUser(ctx context.Context, request UpdateUserRequestObject) (UpdateUserResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return UpdateUser401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return UpdateUser400JSONResponse{Code: 400, Message: "Missing request body"}, nil + } + + // Ensure Username (mirroring legacy validation) + if request.Body.User == "" { + return UpdateUser400JSONResponse{Code: 400, Message: "User cannot be empty"}, nil + } + + var err error + // Handle different operations mirroring legacy appUpdateAdminUsers + switch request.Body.Operation { + case "CREATE": + err = s.createUser(ctx, request.Body.User, request.Body.Password, request.Body.IsAdmin) + case "UPDATE": + err = s.updateUser(ctx, request.Body.User, request.Body.Password, request.Body.IsAdmin) + case "DELETE": + err = s.deleteUser(ctx, request.Body.User) + default: + return UpdateUser400JSONResponse{Code: 400, Message: "Unknown user operation"}, nil + } + + if err != nil { + return UpdateUser500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + // Get updated users list (mirroring legacy appGetAdminUsers) + users, err := s.db.Queries.GetUsers(ctx) + if err != nil { + return UpdateUser500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + apiUsers := make([]User, len(users)) + for i, user := range users { + apiUsers[i] = User{ + Id: user.ID, + Admin: user.Admin, + CreatedAt: parseTime(user.CreatedAt), + } + } + + return UpdateUser200JSONResponse{ + Users: &apiUsers, + }, nil +} + +// createUser creates a new user, mirroring legacy createUser +func (s *Server) createUser(ctx context.Context, user string, rawPassword *string, isAdmin *bool) error { + // Validate Necessary Parameters (mirroring legacy) + if rawPassword == nil || *rawPassword == "" { + return fmt.Errorf("password can't be empty") + } + + // Base Params + createParams := database.CreateUserParams{ + ID: user, + } + + // Handle Admin (Explicit or False) + if isAdmin != nil { + createParams.Admin = *isAdmin + } else { + createParams.Admin = false + } + + // Parse Password (mirroring legacy) + password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword))) + hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams) + if err != nil { + return fmt.Errorf("unable to create hashed password") + } + createParams.Pass = &hashedPassword + + // Generate Auth Hash (mirroring legacy) + rawAuthHash, err := utils.GenerateToken(64) + if err != nil { + return fmt.Errorf("unable to create token for user") + } + authHash := fmt.Sprintf("%x", rawAuthHash) + createParams.AuthHash = &authHash + + // Create user in DB (mirroring legacy) + if rows, err := s.db.Queries.CreateUser(ctx, createParams); err != nil { + return fmt.Errorf("unable to create user") + } else if rows == 0 { + return fmt.Errorf("user already exists") + } + + return nil +} + +// updateUser updates an existing user, mirroring legacy updateUser +func (s *Server) updateUser(ctx context.Context, user string, rawPassword *string, isAdmin *bool) error { + // Validate Necessary Parameters (mirroring legacy) + if rawPassword == nil && isAdmin == nil { + return fmt.Errorf("nothing to update") + } + + // Base Params + updateParams := database.UpdateUserParams{ + UserID: user, + } + + // Handle Admin (Update or Existing) + if isAdmin != nil { + updateParams.Admin = *isAdmin + } else { + userData, err := s.db.Queries.GetUser(ctx, user) + if err != nil { + return fmt.Errorf("unable to get user") + } + updateParams.Admin = userData.Admin + } + + // Check Admins - Disallow Demotion (mirroring legacy isLastAdmin) + if isLast, err := s.isLastAdmin(ctx, user); err != nil { + return err + } else if isLast && !updateParams.Admin { + return fmt.Errorf("unable to demote %s - last admin", user) + } + + // Handle Password (mirroring legacy) + if rawPassword != nil { + if *rawPassword == "" { + return fmt.Errorf("password can't be empty") + } + + // Parse Password + password := fmt.Sprintf("%x", md5.Sum([]byte(*rawPassword))) + hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams) + if err != nil { + return fmt.Errorf("unable to create hashed password") + } + updateParams.Password = &hashedPassword + + // Generate Auth Hash + rawAuthHash, err := utils.GenerateToken(64) + if err != nil { + return fmt.Errorf("unable to create token for user") + } + authHash := fmt.Sprintf("%x", rawAuthHash) + updateParams.AuthHash = &authHash + } + + // Update User (mirroring legacy) + _, err := s.db.Queries.UpdateUser(ctx, updateParams) + if err != nil { + return fmt.Errorf("unable to update user") + } + + return nil +} + +// deleteUser deletes a user, mirroring legacy deleteUser +func (s *Server) deleteUser(ctx context.Context, user string) error { + // Check Admins (mirroring legacy isLastAdmin) + if isLast, err := s.isLastAdmin(ctx, user); err != nil { + return err + } else if isLast { + return fmt.Errorf("unable to delete %s - last admin", user) + } + + // Create Backup File (mirroring legacy) + backupFilePath := filepath.Join(s.cfg.ConfigPath, fmt.Sprintf("backups/AnthoLumeBackup_%s.zip", time.Now().Format("20060102150405"))) + backupFile, err := os.Create(backupFilePath) + if err != nil { + return err + } + defer backupFile.Close() + + // Save Backup File (DB Only) (mirroring legacy) + w := bufio.NewWriter(backupFile) + err = s.createBackup(ctx, w, []string{}) + if err != nil { + return err + } + + // Delete User (mirroring legacy) + _, err = s.db.Queries.DeleteUser(ctx, user) + if err != nil { + return fmt.Errorf("unable to delete user") + } + + return nil +} + +// isLastAdmin checks if the user is the last admin, mirroring legacy isLastAdmin +func (s *Server) isLastAdmin(ctx context.Context, userID string) (bool, error) { + allUsers, err := s.db.Queries.GetUsers(ctx) + if err != nil { + return false, fmt.Errorf("unable to get users") + } + + hasAdmin := false + for _, user := range allUsers { + if user.Admin && user.ID != userID { + hasAdmin = true + break + } + } + + return !hasAdmin, nil +} + +// GET /admin/import +func (s *Server) GetImportDirectory(ctx context.Context, request GetImportDirectoryRequestObject) (GetImportDirectoryResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return GetImportDirectory401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + // Handle select parameter - mirroring legacy appGetAdminImport + if request.Params.Select != nil && *request.Params.Select != "" { + return GetImportDirectory200JSONResponse{ + CurrentPath: request.Params.Select, + Items: &[]DirectoryItem{}, + }, nil + } + + // Default Path (mirroring legacy logic) + directory := "" + if request.Params.Directory != nil && *request.Params.Directory != "" { + directory = *request.Params.Directory + } else { + dPath, err := filepath.Abs(s.cfg.DataPath) + if err != nil { + return GetImportDirectory500JSONResponse{Code: 500, Message: "Unable to get data directory absolute path"}, nil + } + directory = dPath + } + + // Read directory entries (mirroring legacy) + entries, err := os.ReadDir(directory) + if err != nil { + return GetImportDirectory500JSONResponse{Code: 500, Message: "Invalid directory"}, nil + } + + allDirectories := []DirectoryItem{} + for _, e := range entries { + if !e.IsDir() { + continue + } + + name := e.Name() + path := filepath.Join(directory, name) + allDirectories = append(allDirectories, DirectoryItem{ + Name: &name, + Path: &path, + }) + } + + cleanPath := filepath.Clean(directory) + + return GetImportDirectory200JSONResponse{ + CurrentPath: &cleanPath, + Items: &allDirectories, + }, nil +} + +// POST /admin/import +func (s *Server) PostImport(ctx context.Context, request PostImportRequestObject) (PostImportResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return PostImport401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return PostImport400JSONResponse{Code: 400, Message: "Missing request body"}, nil + } + + // Get import directory (mirroring legacy) + importDirectory := filepath.Clean(request.Body.Directory) + + // Get data directory (mirroring legacy) + absoluteDataPath, _ := filepath.Abs(filepath.Join(s.cfg.DataPath, "documents")) + + // Validate different path (mirroring legacy) + if absoluteDataPath == importDirectory { + return PostImport400JSONResponse{Code: 400, Message: "Directory is the same as data path"}, nil + } + + // Do Transaction (mirroring legacy) + tx, err := s.db.DB.Begin() + if err != nil { + return PostImport500JSONResponse{Code: 500, Message: "Unknown error"}, nil + } + + // Defer & Start Transaction (mirroring legacy) + defer func() { + if err := tx.Rollback(); err != nil { + log.Error("DB Rollback Error:", err) + } + }() + qtx := s.db.Queries.WithTx(tx) + + // Track imports (mirroring legacy) + importResults := make([]ImportResult, 0) + + // Walk Directory & Import (mirroring legacy) + err = filepath.WalkDir(importDirectory, func(importPath string, f fs.DirEntry, err error) error { + if err != nil { + return err + } + + if f.IsDir() { + return nil + } + + // Get relative path (mirroring legacy) + basePath := importDirectory + relFilePath, err := filepath.Rel(importDirectory, importPath) + if err != nil { + log.Warnf("path error: %v", err) + return nil + } + + // Track imports (mirroring legacy) + iResult := ImportResult{ + Path: &relFilePath, + } + defer func() { + importResults = append(importResults, iResult) + }() + + // Get metadata (mirroring legacy) + fileMeta, err := metadata.GetMetadata(importPath) + if err != nil { + log.Errorf("metadata error: %v", err) + errMsg := err.Error() + iResult.Error = &errMsg + status := ImportResultStatus("FAILED") + iResult.Status = &status + return nil + } + iResult.Id = fileMeta.PartialMD5 + name := fmt.Sprintf("%s - %s", *fileMeta.Author, *fileMeta.Title) + iResult.Name = &name + + // Check already exists (mirroring legacy) + _, err = qtx.GetDocument(ctx, *fileMeta.PartialMD5) + if err == nil { + log.Warnf("document already exists: %s", *fileMeta.PartialMD5) + status := ImportResultStatus("EXISTS") + iResult.Status = &status + return nil + } + + // Import Copy (mirroring legacy) + if request.Body.Type == "COPY" { + // Derive & Sanitize File Name (mirroring legacy deriveBaseFileName) + relFilePath = s.deriveBaseFileName(fileMeta) + safePath := filepath.Join(s.cfg.DataPath, "documents", relFilePath) + + // Open Source File + srcFile, err := os.Open(importPath) + if err != nil { + log.Errorf("unable to open current file: %v", err) + errMsg := err.Error() + iResult.Error = &errMsg + return nil + } + defer srcFile.Close() + + // Open Destination File + destFile, err := os.Create(safePath) + if err != nil { + log.Errorf("unable to open destination file: %v", err) + errMsg := err.Error() + iResult.Error = &errMsg + return nil + } + defer destFile.Close() + + // Copy File + if _, err = io.Copy(destFile, srcFile); err != nil { + log.Errorf("unable to save file: %v", err) + errMsg := err.Error() + iResult.Error = &errMsg + return nil + } + + // Update Base & Path + basePath = filepath.Join(s.cfg.DataPath, "documents") + iResult.Path = &relFilePath + } + + // Upsert document (mirroring legacy) + if _, err = qtx.UpsertDocument(ctx, database.UpsertDocumentParams{ + ID: *fileMeta.PartialMD5, + Title: fileMeta.Title, + Author: fileMeta.Author, + Description: fileMeta.Description, + Md5: fileMeta.MD5, + Words: fileMeta.WordCount, + Filepath: &relFilePath, + Basepath: &basePath, + }); err != nil { + log.Errorf("UpsertDocument DB Error: %v", err) + errMsg := err.Error() + iResult.Error = &errMsg + return nil + } + + status := ImportResultStatus("SUCCESS") + iResult.Status = &status + return nil + }) + if err != nil { + return PostImport500JSONResponse{Code: 500, Message: fmt.Sprintf("Import Failed: %v", err)}, nil + } + + // Commit transaction (mirroring legacy) + if err := tx.Commit(); err != nil { + log.Error("Transaction Commit DB Error: ", err) + return PostImport500JSONResponse{Code: 500, Message: fmt.Sprintf("Import DB Error: %v", err)}, nil + } + + // Sort import results (mirroring legacy importStatusPriority) + sort.Slice(importResults, func(i int, j int) bool { + return s.importStatusPriority(*importResults[i].Status) < + s.importStatusPriority(*importResults[j].Status) + }) + + return PostImport200JSONResponse{ + Results: &importResults, + }, nil +} + +// importStatusPriority returns the order priority for import status, mirroring legacy +func (s *Server) importStatusPriority(status ImportResultStatus) int { + switch status { + case "FAILED": + return 1 + case "EXISTS": + return 2 + default: + return 3 + } +} + +// deriveBaseFileName builds the base filename for a given MetadataInfo object, mirroring legacy deriveBaseFileName +func (s *Server) deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string { + var newFileName string + if *metadataInfo.Author != "" { + newFileName = newFileName + *metadataInfo.Author + } else { + newFileName = newFileName + "Unknown" + } + if *metadataInfo.Title != "" { + newFileName = newFileName + " - " + *metadataInfo.Title + } else { + newFileName = newFileName + " - Unknown" + } + + // Remove Slashes (mirroring legacy) + fileName := strings.ReplaceAll(newFileName, "/", "") + return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type)) +} + +// GET /admin/import-results +func (s *Server) GetImportResults(ctx context.Context, request GetImportResultsRequestObject) (GetImportResultsResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return GetImportResults401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + // Note: In the legacy implementation, import results are returned directly + // after import. This endpoint could be enhanced to store results in + // session or memory for later retrieval. For now, return empty results. + return GetImportResults200JSONResponse{ + Results: &[]ImportResult{}, + }, nil +} + +// GET /admin/logs +func (s *Server) GetLogs(ctx context.Context, request GetLogsRequestObject) (GetLogsResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return GetLogs401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + page := int64(1) + if request.Params.Page != nil && *request.Params.Page > 0 { + page = *request.Params.Page + } + + limit := int64(100) + if request.Params.Limit != nil && *request.Params.Limit > 0 { + limit = *request.Params.Limit + } + + filter := "" + if request.Params.Filter != nil { + filter = strings.TrimSpace(*request.Params.Filter) + } + + var jqFilter *gojq.Code + var basicFilter string + + // Parse JQ or basic filter (mirroring legacy) + if strings.HasPrefix(filter, "\"") && strings.HasSuffix(filter, "\"") { + basicFilter = filter[1 : len(filter)-1] + } else if filter != "" { + parsed, err := gojq.Parse(filter) + if err != nil { + log.Error("Unable to parse JQ filter") + return GetLogs500JSONResponse{Code: 500, Message: "Unable to parse JQ filter"}, nil + } + + jqFilter, err = gojq.Compile(parsed) + if err != nil { + log.Error("Unable to compile JQ filter") + return GetLogs500JSONResponse{Code: 500, Message: "Unable to compile JQ filter"}, nil + } + } + + logPath := filepath.Join(s.cfg.ConfigPath, "logs/antholume.log") + logFile, err := os.Open(logPath) + if err != nil { + return GetLogs500JSONResponse{Code: 500, Message: "Missing AnthoLume log file"}, nil + } + defer logFile.Close() + + offset := (page - 1) * limit + logLines := make([]string, 0, limit) + matchedCount := int64(0) + + scanner := bufio.NewScanner(logFile) + for scanner.Scan() { + formattedLog, matched := formatLogLine(scanner.Text(), basicFilter, jqFilter) + if !matched { + continue + } + + if matchedCount >= offset && int64(len(logLines)) < limit { + logLines = append(logLines, formattedLog) + } + matchedCount++ + } + + if err := scanner.Err(); err != nil { + return GetLogs500JSONResponse{Code: 500, Message: "Unable to read AnthoLume log file"}, nil + } + + var nextPage *int64 + var previousPage *int64 + if page > 1 { + previousPage = ptrOf(page - 1) + } + if offset+int64(len(logLines)) < matchedCount { + nextPage = ptrOf(page + 1) + } + + return GetLogs200JSONResponse{ + Logs: &logLines, + Filter: &filter, + Page: &page, + Limit: &limit, + NextPage: nextPage, + PreviousPage: previousPage, + Total: &matchedCount, + }, nil +} + +func formatLogLine(rawLog string, basicFilter string, jqFilter *gojq.Code) (string, bool) { + var jsonMap map[string]any + if err := json.Unmarshal([]byte(rawLog), &jsonMap); err != nil { + if basicFilter == "" && jqFilter == nil { + return rawLog, true + } + if basicFilter != "" && strings.Contains(rawLog, basicFilter) { + return rawLog, true + } + return "", false + } + + rawData, err := json.MarshalIndent(jsonMap, "", " ") + if err != nil { + if basicFilter == "" && jqFilter == nil { + return rawLog, true + } + if basicFilter != "" && strings.Contains(rawLog, basicFilter) { + return rawLog, true + } + return "", false + } + + formattedLog := string(rawData) + if basicFilter != "" { + return formattedLog, strings.Contains(formattedLog, basicFilter) + } + if jqFilter == nil { + return formattedLog, true + } + + result, _ := jqFilter.Run(jsonMap).Next() + if _, ok := result.(error); ok { + return formattedLog, true + } + if result == nil { + return "", false + } + + filteredData, err := json.MarshalIndent(result, "", " ") + if err == nil { + formattedLog = string(filteredData) + } + + return formattedLog, true +} diff --git a/api/v1/admin_test.go b/api/v1/admin_test.go new file mode 100644 index 0000000..d46573b --- /dev/null +++ b/api/v1/admin_test.go @@ -0,0 +1,152 @@ +package v1 + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + argon2 "github.com/alexedwards/argon2id" + "github.com/stretchr/testify/require" + "reichard.io/antholume/config" + "reichard.io/antholume/database" +) + +func createAdminTestUser(t *testing.T, db *database.DBManager, username, password string) { + t.Helper() + + md5Hash := fmt.Sprintf("%x", md5.Sum([]byte(password))) + hashedPassword, err := argon2.CreateHash(md5Hash, argon2.DefaultParams) + require.NoError(t, err) + + authHash := "test-auth-hash" + _, err = db.Queries.CreateUser(context.Background(), database.CreateUserParams{ + ID: username, + Pass: &hashedPassword, + AuthHash: &authHash, + Admin: true, + }) + require.NoError(t, err) +} + +func loginAdminTestUser(t *testing.T, srv *Server, username, password string) *http.Cookie { + t.Helper() + + body, err := json.Marshal(LoginRequest{Username: username, Password: password}) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) + w := httptest.NewRecorder() + srv.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + cookies := w.Result().Cookies() + require.Len(t, cookies, 1) + + return cookies[0] +} + +func TestGetLogsPagination(t *testing.T) { + configPath := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(configPath, "logs"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(configPath, "logs", "antholume.log"), []byte( + "{\"level\":\"info\",\"msg\":\"one\"}\n"+ + "plain two\n"+ + "{\"level\":\"error\",\"msg\":\"three\"}\n"+ + "plain four\n", + ), 0o644)) + + cfg := &config.Config{ + ListenPort: "8080", + DBType: "memory", + DBName: "test", + ConfigPath: configPath, + CookieAuthKey: "test-auth-key-32-bytes-long-enough", + CookieEncKey: "0123456789abcdef", + CookieSecure: false, + CookieHTTPOnly: true, + Version: "test", + DemoMode: false, + RegistrationEnabled: true, + } + + db := database.NewMgr(cfg) + srv := NewServer(db, cfg, nil) + createAdminTestUser(t, db, "admin", "password") + cookie := loginAdminTestUser(t, srv, "admin", "password") + + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/logs?page=2&limit=2", nil) + req.AddCookie(cookie) + w := httptest.NewRecorder() + srv.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + + var resp LogsResponse + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + require.NotNil(t, resp.Logs) + require.Len(t, *resp.Logs, 2) + require.NotNil(t, resp.Page) + require.Equal(t, int64(2), *resp.Page) + require.NotNil(t, resp.Limit) + require.Equal(t, int64(2), *resp.Limit) + require.NotNil(t, resp.Total) + require.Equal(t, int64(4), *resp.Total) + require.Nil(t, resp.NextPage) + require.NotNil(t, resp.PreviousPage) + require.Equal(t, int64(1), *resp.PreviousPage) + require.Contains(t, (*resp.Logs)[0], "three") + require.Contains(t, (*resp.Logs)[1], "plain four") +} + +func TestGetLogsPaginationWithBasicFilter(t *testing.T) { + configPath := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(configPath, "logs"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(configPath, "logs", "antholume.log"), []byte( + "{\"level\":\"info\",\"msg\":\"match-1\"}\n"+ + "{\"level\":\"info\",\"msg\":\"skip\"}\n"+ + "plain match-2\n"+ + "{\"level\":\"info\",\"msg\":\"match-3\"}\n", + ), 0o644)) + + cfg := &config.Config{ + ListenPort: "8080", + DBType: "memory", + DBName: "test", + ConfigPath: configPath, + CookieAuthKey: "test-auth-key-32-bytes-long-enough", + CookieEncKey: "0123456789abcdef", + CookieSecure: false, + CookieHTTPOnly: true, + Version: "test", + DemoMode: false, + RegistrationEnabled: true, + } + + db := database.NewMgr(cfg) + srv := NewServer(db, cfg, nil) + createAdminTestUser(t, db, "admin", "password") + cookie := loginAdminTestUser(t, srv, "admin", "password") + + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/logs?filter=%22match%22&page=1&limit=2", nil) + req.AddCookie(cookie) + w := httptest.NewRecorder() + srv.ServeHTTP(w, req) + + require.Equal(t, http.StatusOK, w.Code) + + var resp LogsResponse + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + require.NotNil(t, resp.Logs) + require.Len(t, *resp.Logs, 2) + require.NotNil(t, resp.Total) + require.Equal(t, int64(3), *resp.Total) + require.NotNil(t, resp.NextPage) + require.Equal(t, int64(2), *resp.NextPage) +} diff --git a/api/v1/api.gen.go b/api/v1/api.gen.go new file mode 100644 index 0000000..7a3dba6 --- /dev/null +++ b/api/v1/api.gen.go @@ -0,0 +1,4146 @@ +//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" + "io" + "mime/multipart" + "net/http" + "time" + + "github.com/oapi-codegen/runtime" + strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" + openapi_types "github.com/oapi-codegen/runtime/types" +) + +const ( + BearerAuthScopes = "BearerAuth.Scopes" +) + +// Defines values for BackupType. +const ( + COVERS BackupType = "COVERS" + DOCUMENTS BackupType = "DOCUMENTS" +) + +// Valid indicates whether the value is a known member of the BackupType enum. +func (e BackupType) Valid() bool { + switch e { + case COVERS: + return true + case DOCUMENTS: + return true + default: + return false + } +} + +// Defines values for ImportResultStatus. +const ( + EXISTS ImportResultStatus = "EXISTS" + FAILED ImportResultStatus = "FAILED" + SUCCESS ImportResultStatus = "SUCCESS" +) + +// Valid indicates whether the value is a known member of the ImportResultStatus enum. +func (e ImportResultStatus) Valid() bool { + switch e { + case EXISTS: + return true + case FAILED: + return true + case SUCCESS: + return true + default: + return false + } +} + +// Defines values for ImportType. +const ( + COPY ImportType = "COPY" + DIRECT ImportType = "DIRECT" +) + +// Valid indicates whether the value is a known member of the ImportType enum. +func (e ImportType) Valid() bool { + switch e { + case COPY: + return true + case DIRECT: + return true + default: + return false + } +} + +// Defines values for OperationType. +const ( + CREATE OperationType = "CREATE" + DELETE OperationType = "DELETE" + UPDATE OperationType = "UPDATE" +) + +// Valid indicates whether the value is a known member of the OperationType enum. +func (e OperationType) Valid() bool { + switch e { + case CREATE: + return true + case DELETE: + return true + case UPDATE: + return true + default: + return false + } +} + +// Defines values for PostAdminActionMultipartBodyAction. +const ( + BACKUP PostAdminActionMultipartBodyAction = "BACKUP" + CACHETABLES PostAdminActionMultipartBodyAction = "CACHE_TABLES" + METADATAMATCH PostAdminActionMultipartBodyAction = "METADATA_MATCH" + RESTORE PostAdminActionMultipartBodyAction = "RESTORE" +) + +// Valid indicates whether the value is a known member of the PostAdminActionMultipartBodyAction enum. +func (e PostAdminActionMultipartBodyAction) Valid() bool { + switch e { + case BACKUP: + return true + case CACHETABLES: + return true + case METADATAMATCH: + return true + case RESTORE: + return true + default: + return false + } +} + +// Defines values for GetSearchParamsSource. +const ( + AnnasArchive GetSearchParamsSource = "Annas Archive" + LibGen GetSearchParamsSource = "LibGen" +) + +// Valid indicates whether the value is a known member of the GetSearchParamsSource enum. +func (e GetSearchParamsSource) Valid() bool { + switch e { + case AnnasArchive: + return true + case LibGen: + return true + default: + return false + } +} + +// Activity defines model for Activity. +type Activity struct { + Author *string `json:"author,omitempty"` + DeviceId string `json:"device_id"` + DocumentId string `json:"document_id"` + Duration int64 `json:"duration"` + EndPercentage float32 `json:"end_percentage"` + ReadPercentage float32 `json:"read_percentage"` + StartPercentage float32 `json:"start_percentage"` + StartTime string `json:"start_time"` + Title *string `json:"title,omitempty"` +} + +// ActivityResponse defines model for ActivityResponse. +type ActivityResponse struct { + Activities []Activity `json:"activities"` +} + +// BackupType defines model for BackupType. +type BackupType string + +// CreateActivityItem defines model for CreateActivityItem. +type CreateActivityItem struct { + DocumentId string `json:"document_id"` + Duration int64 `json:"duration"` + Page int64 `json:"page"` + Pages int64 `json:"pages"` + StartTime int64 `json:"start_time"` +} + +// CreateActivityRequest defines model for CreateActivityRequest. +type CreateActivityRequest struct { + Activity []CreateActivityItem `json:"activity"` + DeviceId string `json:"device_id"` + DeviceName string `json:"device_name"` +} + +// CreateActivityResponse defines model for CreateActivityResponse. +type CreateActivityResponse struct { + Added int64 `json:"added"` +} + +// DatabaseInfo defines model for DatabaseInfo. +type DatabaseInfo struct { + ActivitySize int64 `json:"activity_size"` + DevicesSize int64 `json:"devices_size"` + DocumentsSize int64 `json:"documents_size"` + ProgressSize int64 `json:"progress_size"` +} + +// Device defines model for Device. +type Device struct { + CreatedAt *time.Time `json:"created_at,omitempty"` + DeviceName *string `json:"device_name,omitempty"` + Id *string `json:"id,omitempty"` + LastSynced *time.Time `json:"last_synced,omitempty"` +} + +// DirectoryItem defines model for DirectoryItem. +type DirectoryItem struct { + Name *string `json:"name,omitempty"` + Path *string `json:"path,omitempty"` +} + +// DirectoryListResponse defines model for DirectoryListResponse. +type DirectoryListResponse struct { + CurrentPath *string `json:"current_path,omitempty"` + Items *[]DirectoryItem `json:"items,omitempty"` +} + +// Document defines model for Document. +type Document struct { + Author string `json:"author"` + CreatedAt time.Time `json:"created_at"` + Deleted bool `json:"deleted"` + Description *string `json:"description,omitempty"` + Filepath *string `json:"filepath,omitempty"` + Id string `json:"id"` + Isbn10 *string `json:"isbn10,omitempty"` + Isbn13 *string `json:"isbn13,omitempty"` + LastRead *time.Time `json:"last_read,omitempty"` + Percentage *float32 `json:"percentage,omitempty"` + SecondsPerPercent *int64 `json:"seconds_per_percent,omitempty"` + Title string `json:"title"` + TotalTimeSeconds *int64 `json:"total_time_seconds,omitempty"` + UpdatedAt time.Time `json:"updated_at"` + Words *int64 `json:"words,omitempty"` + Wpm *float32 `json:"wpm,omitempty"` +} + +// DocumentResponse defines model for DocumentResponse. +type DocumentResponse struct { + Document Document `json:"document"` +} + +// 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"` +} + +// ErrorResponse defines model for ErrorResponse. +type ErrorResponse struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// GraphDataPoint defines model for GraphDataPoint. +type GraphDataPoint struct { + Date string `json:"date"` + MinutesRead int64 `json:"minutes_read"` +} + +// GraphDataResponse defines model for GraphDataResponse. +type GraphDataResponse struct { + GraphData []GraphDataPoint `json:"graph_data"` +} + +// HomeResponse defines model for HomeResponse. +type HomeResponse struct { + DatabaseInfo DatabaseInfo `json:"database_info"` + GraphData GraphDataResponse `json:"graph_data"` + Streaks StreaksResponse `json:"streaks"` + UserStatistics UserStatisticsResponse `json:"user_statistics"` +} + +// ImportResult defines model for ImportResult. +type ImportResult struct { + Error *string `json:"error,omitempty"` + Id *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Path *string `json:"path,omitempty"` + Status *ImportResultStatus `json:"status,omitempty"` +} + +// ImportResultStatus defines model for ImportResult.Status. +type ImportResultStatus string + +// ImportResultsResponse defines model for ImportResultsResponse. +type ImportResultsResponse struct { + Results *[]ImportResult `json:"results,omitempty"` +} + +// ImportType defines model for ImportType. +type ImportType string + +// InfoResponse defines model for InfoResponse. +type InfoResponse struct { + RegistrationEnabled bool `json:"registration_enabled"` + SearchEnabled bool `json:"search_enabled"` + Version string `json:"version"` +} + +// LeaderboardData defines model for LeaderboardData. +type LeaderboardData struct { + All []LeaderboardEntry `json:"all"` + Month []LeaderboardEntry `json:"month"` + Week []LeaderboardEntry `json:"week"` + Year []LeaderboardEntry `json:"year"` +} + +// LeaderboardEntry defines model for LeaderboardEntry. +type LeaderboardEntry struct { + UserId string `json:"user_id"` + Value float64 `json:"value"` +} + +// LogEntry defines model for LogEntry. +type LogEntry = string + +// 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"` +} + +// LogsResponse defines model for LogsResponse. +type LogsResponse struct { + Filter *string `json:"filter,omitempty"` + Limit *int64 `json:"limit,omitempty"` + Logs *[]LogEntry `json:"logs,omitempty"` + NextPage *int64 `json:"next_page,omitempty"` + Page *int64 `json:"page,omitempty"` + PreviousPage *int64 `json:"previous_page,omitempty"` + Total *int64 `json:"total,omitempty"` +} + +// MessageResponse defines model for MessageResponse. +type MessageResponse struct { + Message string `json:"message"` +} + +// OperationType defines model for OperationType. +type OperationType string + +// Progress defines model for Progress. +type Progress struct { + Author *string `json:"author,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + DeviceId *string `json:"device_id,omitempty"` + DeviceName *string `json:"device_name,omitempty"` + DocumentId *string `json:"document_id,omitempty"` + Percentage *float64 `json:"percentage,omitempty"` + Progress *string `json:"progress,omitempty"` + Title *string `json:"title,omitempty"` + UserId *string `json:"user_id,omitempty"` +} + +// ProgressListResponse defines model for ProgressListResponse. +type ProgressListResponse struct { + Limit *int64 `json:"limit,omitempty"` + NextPage *int64 `json:"next_page,omitempty"` + Page *int64 `json:"page,omitempty"` + PreviousPage *int64 `json:"previous_page,omitempty"` + Progress *[]Progress `json:"progress,omitempty"` + Total *int64 `json:"total,omitempty"` +} + +// ProgressResponse defines model for ProgressResponse. +type ProgressResponse struct { + Progress *Progress `json:"progress,omitempty"` +} + +// SearchItem defines model for SearchItem. +type SearchItem struct { + Author *string `json:"author,omitempty"` + FileSize *string `json:"file_size,omitempty"` + FileType *string `json:"file_type,omitempty"` + Id *string `json:"id,omitempty"` + Language *string `json:"language,omitempty"` + Series *string `json:"series,omitempty"` + Title *string `json:"title,omitempty"` + UploadDate *string `json:"upload_date,omitempty"` +} + +// SearchResponse defines model for SearchResponse. +type SearchResponse struct { + Query string `json:"query"` + Results []SearchItem `json:"results"` + Source string `json:"source"` +} + +// SettingsResponse defines model for SettingsResponse. +type SettingsResponse struct { + Devices *[]Device `json:"devices,omitempty"` + Timezone *string `json:"timezone,omitempty"` + User UserData `json:"user"` +} + +// StreaksResponse defines model for StreaksResponse. +type StreaksResponse struct { + Streaks []UserStreak `json:"streaks"` +} + +// UpdateProgressRequest defines model for UpdateProgressRequest. +type UpdateProgressRequest struct { + DeviceId string `json:"device_id"` + DeviceName string `json:"device_name"` + DocumentId string `json:"document_id"` + Percentage float64 `json:"percentage"` + Progress string `json:"progress"` +} + +// UpdateProgressResponse defines model for UpdateProgressResponse. +type UpdateProgressResponse struct { + DocumentId string `json:"document_id"` + Timestamp time.Time `json:"timestamp"` +} + +// UpdateSettingsRequest defines model for UpdateSettingsRequest. +type UpdateSettingsRequest struct { + NewPassword *string `json:"new_password,omitempty"` + Password *string `json:"password,omitempty"` + Timezone *string `json:"timezone,omitempty"` +} + +// User defines model for User. +type User struct { + Admin bool `json:"admin"` + CreatedAt time.Time `json:"created_at"` + Id string `json:"id"` +} + +// UserData defines model for UserData. +type UserData struct { + IsAdmin bool `json:"is_admin"` + Username string `json:"username"` +} + +// UserStatisticsResponse defines model for UserStatisticsResponse. +type UserStatisticsResponse struct { + Duration LeaderboardData `json:"duration"` + Words LeaderboardData `json:"words"` + Wpm LeaderboardData `json:"wpm"` +} + +// UserStreak defines model for UserStreak. +type UserStreak struct { + CurrentStreak int64 `json:"current_streak"` + CurrentStreakEndDate string `json:"current_streak_end_date"` + CurrentStreakStartDate string `json:"current_streak_start_date"` + MaxStreak int64 `json:"max_streak"` + MaxStreakEndDate string `json:"max_streak_end_date"` + MaxStreakStartDate string `json:"max_streak_start_date"` + Window string `json:"window"` +} + +// UsersResponse defines model for UsersResponse. +type UsersResponse struct { + Users *[]User `json:"users,omitempty"` +} + +// 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"` +} + +// PostAdminActionMultipartBody defines parameters for PostAdminAction. +type PostAdminActionMultipartBody struct { + Action PostAdminActionMultipartBodyAction `json:"action"` + BackupTypes *[]BackupType `json:"backup_types,omitempty"` + RestoreFile *openapi_types.File `json:"restore_file,omitempty"` +} + +// PostAdminActionMultipartBodyAction defines parameters for PostAdminAction. +type PostAdminActionMultipartBodyAction string + +// GetImportDirectoryParams defines parameters for GetImportDirectory. +type GetImportDirectoryParams struct { + Directory *string `form:"directory,omitempty" json:"directory,omitempty"` + Select *string `form:"select,omitempty" json:"select,omitempty"` +} + +// PostImportFormdataBody defines parameters for PostImport. +type PostImportFormdataBody struct { + Directory string `form:"directory" json:"directory"` + Type ImportType `form:"type" json:"type"` +} + +// GetLogsParams defines parameters for GetLogs. +type GetLogsParams struct { + Filter *string `form:"filter,omitempty" json:"filter,omitempty"` + Page *int64 `form:"page,omitempty" json:"page,omitempty"` + Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` +} + +// UpdateUserFormdataBody defines parameters for UpdateUser. +type UpdateUserFormdataBody struct { + IsAdmin *bool `form:"is_admin,omitempty" json:"is_admin,omitempty"` + Operation OperationType `form:"operation" json:"operation"` + Password *string `form:"password,omitempty" json:"password,omitempty"` + User string `form:"user" json:"user"` +} + +// 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"` +} + +// CreateDocumentMultipartBody defines parameters for CreateDocument. +type CreateDocumentMultipartBody struct { + DocumentFile openapi_types.File `json:"document_file"` +} + +// EditDocumentJSONBody defines parameters for EditDocument. +type EditDocumentJSONBody struct { + Author *string `json:"author,omitempty"` + CoverGbid *string `json:"cover_gbid,omitempty"` + Description *string `json:"description,omitempty"` + Isbn10 *string `json:"isbn10,omitempty"` + Isbn13 *string `json:"isbn13,omitempty"` + Title *string `json:"title,omitempty"` +} + +// UploadDocumentCoverMultipartBody defines parameters for UploadDocumentCover. +type UploadDocumentCoverMultipartBody struct { + CoverFile openapi_types.File `json:"cover_file"` +} + +// GetProgressListParams defines parameters for GetProgressList. +type GetProgressListParams struct { + Page *int64 `form:"page,omitempty" json:"page,omitempty"` + Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` + Document *string `form:"document,omitempty" json:"document,omitempty"` +} + +// GetSearchParams defines parameters for GetSearch. +type GetSearchParams struct { + Query string `form:"query" json:"query"` + Source GetSearchParamsSource `form:"source" json:"source"` +} + +// GetSearchParamsSource defines parameters for GetSearch. +type GetSearchParamsSource string + +// PostSearchFormdataBody defines parameters for PostSearch. +type PostSearchFormdataBody struct { + Author string `form:"author" json:"author"` + Id string `form:"id" json:"id"` + Source string `form:"source" json:"source"` + Title string `form:"title" json:"title"` +} + +// CreateActivityJSONRequestBody defines body for CreateActivity for application/json ContentType. +type CreateActivityJSONRequestBody = CreateActivityRequest + +// PostAdminActionMultipartRequestBody defines body for PostAdminAction for multipart/form-data ContentType. +type PostAdminActionMultipartRequestBody PostAdminActionMultipartBody + +// PostImportFormdataRequestBody defines body for PostImport for application/x-www-form-urlencoded ContentType. +type PostImportFormdataRequestBody PostImportFormdataBody + +// UpdateUserFormdataRequestBody defines body for UpdateUser for application/x-www-form-urlencoded ContentType. +type UpdateUserFormdataRequestBody UpdateUserFormdataBody + +// LoginJSONRequestBody defines body for Login for application/json ContentType. +type LoginJSONRequestBody = LoginRequest + +// RegisterJSONRequestBody defines body for Register for application/json ContentType. +type RegisterJSONRequestBody = LoginRequest + +// CreateDocumentMultipartRequestBody defines body for CreateDocument for multipart/form-data ContentType. +type CreateDocumentMultipartRequestBody CreateDocumentMultipartBody + +// EditDocumentJSONRequestBody defines body for EditDocument for application/json ContentType. +type EditDocumentJSONRequestBody EditDocumentJSONBody + +// UploadDocumentCoverMultipartRequestBody defines body for UploadDocumentCover for multipart/form-data ContentType. +type UploadDocumentCoverMultipartRequestBody UploadDocumentCoverMultipartBody + +// UpdateProgressJSONRequestBody defines body for UpdateProgress for application/json ContentType. +type UpdateProgressJSONRequestBody = UpdateProgressRequest + +// PostSearchFormdataRequestBody defines body for PostSearch for application/x-www-form-urlencoded ContentType. +type PostSearchFormdataRequestBody PostSearchFormdataBody + +// UpdateSettingsJSONRequestBody defines body for UpdateSettings for application/json ContentType. +type UpdateSettingsJSONRequestBody = UpdateSettingsRequest + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Get activity data + // (GET /activity) + GetActivity(w http.ResponseWriter, r *http.Request, params GetActivityParams) + // Create activity records + // (POST /activity) + CreateActivity(w http.ResponseWriter, r *http.Request) + // Get admin page data + // (GET /admin) + GetAdmin(w http.ResponseWriter, r *http.Request) + // Perform admin action (backup, restore, etc.) + // (POST /admin) + PostAdminAction(w http.ResponseWriter, r *http.Request) + // Get import directory list + // (GET /admin/import) + GetImportDirectory(w http.ResponseWriter, r *http.Request, params GetImportDirectoryParams) + // Perform import + // (POST /admin/import) + PostImport(w http.ResponseWriter, r *http.Request) + // Get import results + // (GET /admin/import-results) + GetImportResults(w http.ResponseWriter, r *http.Request) + // Get logs with optional filter + // (GET /admin/logs) + GetLogs(w http.ResponseWriter, r *http.Request, params GetLogsParams) + // Get all users + // (GET /admin/users) + GetUsers(w http.ResponseWriter, r *http.Request) + // Create, update, or delete user + // (POST /admin/users) + UpdateUser(w http.ResponseWriter, r *http.Request) + // 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) + // User registration + // (POST /auth/register) + Register(w http.ResponseWriter, r *http.Request) + // List documents + // (GET /documents) + GetDocuments(w http.ResponseWriter, r *http.Request, params GetDocumentsParams) + // Upload a new document + // (POST /documents) + CreateDocument(w http.ResponseWriter, r *http.Request) + // Get a single document + // (GET /documents/{id}) + GetDocument(w http.ResponseWriter, r *http.Request, id string) + // Update document editable fields + // (POST /documents/{id}) + EditDocument(w http.ResponseWriter, r *http.Request, id string) + // Get document cover image + // (GET /documents/{id}/cover) + GetDocumentCover(w http.ResponseWriter, r *http.Request, id string) + // Upload document cover image + // (POST /documents/{id}/cover) + UploadDocumentCover(w http.ResponseWriter, r *http.Request, id string) + // Download document file + // (GET /documents/{id}/file) + GetDocumentFile(w http.ResponseWriter, r *http.Request, id string) + // Get home page data + // (GET /home) + GetHome(w http.ResponseWriter, r *http.Request) + // Get daily read stats graph data + // (GET /home/graph) + GetGraphData(w http.ResponseWriter, r *http.Request) + // Get user statistics (leaderboards) + // (GET /home/statistics) + GetUserStatistics(w http.ResponseWriter, r *http.Request) + // Get user streaks + // (GET /home/streaks) + GetStreaks(w http.ResponseWriter, r *http.Request) + // Get server information + // (GET /info) + GetInfo(w http.ResponseWriter, r *http.Request) + // List progress records + // (GET /progress) + GetProgressList(w http.ResponseWriter, r *http.Request, params GetProgressListParams) + // Update document progress + // (PUT /progress) + UpdateProgress(w http.ResponseWriter, r *http.Request) + // Get document progress + // (GET /progress/{id}) + GetProgress(w http.ResponseWriter, r *http.Request, id string) + // Search external book sources + // (GET /search) + GetSearch(w http.ResponseWriter, r *http.Request, params GetSearchParams) + // Download search result + // (POST /search) + PostSearch(w http.ResponseWriter, r *http.Request) + // Get user settings + // (GET /settings) + GetSettings(w http.ResponseWriter, r *http.Request) + // Update user settings + // (PUT /settings) + UpdateSettings(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) +} + +// CreateActivity operation middleware +func (siw *ServerInterfaceWrapper) CreateActivity(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.CreateActivity(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetAdmin operation middleware +func (siw *ServerInterfaceWrapper) GetAdmin(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.GetAdmin(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PostAdminAction operation middleware +func (siw *ServerInterfaceWrapper) PostAdminAction(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.PostAdminAction(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetImportDirectory operation middleware +func (siw *ServerInterfaceWrapper) GetImportDirectory(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 GetImportDirectoryParams + + // ------------- Optional query parameter "directory" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "directory", r.URL.Query(), ¶ms.Directory, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "directory", Err: err}) + return + } + + // ------------- Optional query parameter "select" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "select", r.URL.Query(), ¶ms.Select, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "select", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetImportDirectory(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PostImport operation middleware +func (siw *ServerInterfaceWrapper) PostImport(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.PostImport(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetImportResults operation middleware +func (siw *ServerInterfaceWrapper) GetImportResults(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.GetImportResults(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetLogs operation middleware +func (siw *ServerInterfaceWrapper) GetLogs(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 GetLogsParams + + // ------------- Optional query parameter "filter" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "filter", r.URL.Query(), ¶ms.Filter, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter", Err: err}) + return + } + + // ------------- 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 + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetLogs(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetUsers operation middleware +func (siw *ServerInterfaceWrapper) GetUsers(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.GetUsers(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// UpdateUser operation middleware +func (siw *ServerInterfaceWrapper) UpdateUser(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.UpdateUser(w, r) + })) + + 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) +} + +// Register operation middleware +func (siw *ServerInterfaceWrapper) Register(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.Register(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) +} + +// CreateDocument operation middleware +func (siw *ServerInterfaceWrapper) CreateDocument(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.CreateDocument(w, r) + })) + + 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) +} + +// EditDocument operation middleware +func (siw *ServerInterfaceWrapper) EditDocument(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.EditDocument(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetDocumentCover operation middleware +func (siw *ServerInterfaceWrapper) GetDocumentCover(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.GetDocumentCover(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// UploadDocumentCover operation middleware +func (siw *ServerInterfaceWrapper) UploadDocumentCover(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.UploadDocumentCover(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetDocumentFile operation middleware +func (siw *ServerInterfaceWrapper) GetDocumentFile(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.GetDocumentFile(w, r, id) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetHome operation middleware +func (siw *ServerInterfaceWrapper) GetHome(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.GetHome(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetGraphData operation middleware +func (siw *ServerInterfaceWrapper) GetGraphData(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.GetGraphData(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetUserStatistics operation middleware +func (siw *ServerInterfaceWrapper) GetUserStatistics(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.GetUserStatistics(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetStreaks operation middleware +func (siw *ServerInterfaceWrapper) GetStreaks(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.GetStreaks(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetInfo operation middleware +func (siw *ServerInterfaceWrapper) GetInfo(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetInfo(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetProgressList operation middleware +func (siw *ServerInterfaceWrapper) GetProgressList(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 GetProgressListParams + + // ------------- 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 "document" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "document", r.URL.Query(), ¶ms.Document, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "document", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetProgressList(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// UpdateProgress operation middleware +func (siw *ServerInterfaceWrapper) UpdateProgress(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.UpdateProgress(w, r) + })) + + 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) +} + +// GetSearch operation middleware +func (siw *ServerInterfaceWrapper) GetSearch(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 GetSearchParams + + // ------------- Required query parameter "query" ------------- + + if paramValue := r.URL.Query().Get("query"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "query"}) + return + } + + err = runtime.BindQueryParameterWithOptions("form", true, true, "query", r.URL.Query(), ¶ms.Query, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "query", Err: err}) + return + } + + // ------------- Required query parameter "source" ------------- + + if paramValue := r.URL.Query().Get("source"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "source"}) + return + } + + err = runtime.BindQueryParameterWithOptions("form", true, true, "source", r.URL.Query(), ¶ms.Source, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "source", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetSearch(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PostSearch operation middleware +func (siw *ServerInterfaceWrapper) PostSearch(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.PostSearch(w, r) + })) + + 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) +} + +// UpdateSettings operation middleware +func (siw *ServerInterfaceWrapper) UpdateSettings(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.UpdateSettings(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+"/activity", wrapper.CreateActivity) + m.HandleFunc("GET "+options.BaseURL+"/admin", wrapper.GetAdmin) + m.HandleFunc("POST "+options.BaseURL+"/admin", wrapper.PostAdminAction) + m.HandleFunc("GET "+options.BaseURL+"/admin/import", wrapper.GetImportDirectory) + m.HandleFunc("POST "+options.BaseURL+"/admin/import", wrapper.PostImport) + m.HandleFunc("GET "+options.BaseURL+"/admin/import-results", wrapper.GetImportResults) + m.HandleFunc("GET "+options.BaseURL+"/admin/logs", wrapper.GetLogs) + m.HandleFunc("GET "+options.BaseURL+"/admin/users", wrapper.GetUsers) + m.HandleFunc("POST "+options.BaseURL+"/admin/users", wrapper.UpdateUser) + 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("POST "+options.BaseURL+"/auth/register", wrapper.Register) + m.HandleFunc("GET "+options.BaseURL+"/documents", wrapper.GetDocuments) + m.HandleFunc("POST "+options.BaseURL+"/documents", wrapper.CreateDocument) + m.HandleFunc("GET "+options.BaseURL+"/documents/{id}", wrapper.GetDocument) + m.HandleFunc("POST "+options.BaseURL+"/documents/{id}", wrapper.EditDocument) + m.HandleFunc("GET "+options.BaseURL+"/documents/{id}/cover", wrapper.GetDocumentCover) + m.HandleFunc("POST "+options.BaseURL+"/documents/{id}/cover", wrapper.UploadDocumentCover) + m.HandleFunc("GET "+options.BaseURL+"/documents/{id}/file", wrapper.GetDocumentFile) + m.HandleFunc("GET "+options.BaseURL+"/home", wrapper.GetHome) + m.HandleFunc("GET "+options.BaseURL+"/home/graph", wrapper.GetGraphData) + m.HandleFunc("GET "+options.BaseURL+"/home/statistics", wrapper.GetUserStatistics) + m.HandleFunc("GET "+options.BaseURL+"/home/streaks", wrapper.GetStreaks) + m.HandleFunc("GET "+options.BaseURL+"/info", wrapper.GetInfo) + m.HandleFunc("GET "+options.BaseURL+"/progress", wrapper.GetProgressList) + m.HandleFunc("PUT "+options.BaseURL+"/progress", wrapper.UpdateProgress) + m.HandleFunc("GET "+options.BaseURL+"/progress/{id}", wrapper.GetProgress) + m.HandleFunc("GET "+options.BaseURL+"/search", wrapper.GetSearch) + m.HandleFunc("POST "+options.BaseURL+"/search", wrapper.PostSearch) + m.HandleFunc("GET "+options.BaseURL+"/settings", wrapper.GetSettings) + m.HandleFunc("PUT "+options.BaseURL+"/settings", wrapper.UpdateSettings) + + 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 CreateActivityRequestObject struct { + Body *CreateActivityJSONRequestBody +} + +type CreateActivityResponseObject interface { + VisitCreateActivityResponse(w http.ResponseWriter) error +} + +type CreateActivity200JSONResponse CreateActivityResponse + +func (response CreateActivity200JSONResponse) VisitCreateActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type CreateActivity400JSONResponse ErrorResponse + +func (response CreateActivity400JSONResponse) VisitCreateActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CreateActivity401JSONResponse ErrorResponse + +func (response CreateActivity401JSONResponse) VisitCreateActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type CreateActivity500JSONResponse ErrorResponse + +func (response CreateActivity500JSONResponse) VisitCreateActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetAdminRequestObject struct { +} + +type GetAdminResponseObject interface { + VisitGetAdminResponse(w http.ResponseWriter) error +} + +type GetAdmin200JSONResponse struct { + DatabaseInfo *DatabaseInfo `json:"database_info,omitempty"` +} + +func (response GetAdmin200JSONResponse) VisitGetAdminResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetAdmin401JSONResponse ErrorResponse + +func (response GetAdmin401JSONResponse) VisitGetAdminResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type PostAdminActionRequestObject struct { + Body *multipart.Reader +} + +type PostAdminActionResponseObject interface { + VisitPostAdminActionResponse(w http.ResponseWriter) error +} + +type PostAdminAction200JSONResponse MessageResponse + +func (response PostAdminAction200JSONResponse) VisitPostAdminActionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type PostAdminAction200ApplicationoctetStreamResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response PostAdminAction200ApplicationoctetStreamResponse) VisitPostAdminActionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/octet-stream") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +type PostAdminAction400JSONResponse ErrorResponse + +func (response PostAdminAction400JSONResponse) VisitPostAdminActionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type PostAdminAction401JSONResponse ErrorResponse + +func (response PostAdminAction401JSONResponse) VisitPostAdminActionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type PostAdminAction500JSONResponse ErrorResponse + +func (response PostAdminAction500JSONResponse) VisitPostAdminActionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetImportDirectoryRequestObject struct { + Params GetImportDirectoryParams +} + +type GetImportDirectoryResponseObject interface { + VisitGetImportDirectoryResponse(w http.ResponseWriter) error +} + +type GetImportDirectory200JSONResponse DirectoryListResponse + +func (response GetImportDirectory200JSONResponse) VisitGetImportDirectoryResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetImportDirectory401JSONResponse ErrorResponse + +func (response GetImportDirectory401JSONResponse) VisitGetImportDirectoryResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetImportDirectory500JSONResponse ErrorResponse + +func (response GetImportDirectory500JSONResponse) VisitGetImportDirectoryResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type PostImportRequestObject struct { + Body *PostImportFormdataRequestBody +} + +type PostImportResponseObject interface { + VisitPostImportResponse(w http.ResponseWriter) error +} + +type PostImport200JSONResponse ImportResultsResponse + +func (response PostImport200JSONResponse) VisitPostImportResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type PostImport400JSONResponse ErrorResponse + +func (response PostImport400JSONResponse) VisitPostImportResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type PostImport401JSONResponse ErrorResponse + +func (response PostImport401JSONResponse) VisitPostImportResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type PostImport500JSONResponse ErrorResponse + +func (response PostImport500JSONResponse) VisitPostImportResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetImportResultsRequestObject struct { +} + +type GetImportResultsResponseObject interface { + VisitGetImportResultsResponse(w http.ResponseWriter) error +} + +type GetImportResults200JSONResponse ImportResultsResponse + +func (response GetImportResults200JSONResponse) VisitGetImportResultsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetImportResults401JSONResponse ErrorResponse + +func (response GetImportResults401JSONResponse) VisitGetImportResultsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetImportResults500JSONResponse ErrorResponse + +func (response GetImportResults500JSONResponse) VisitGetImportResultsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetLogsRequestObject struct { + Params GetLogsParams +} + +type GetLogsResponseObject interface { + VisitGetLogsResponse(w http.ResponseWriter) error +} + +type GetLogs200JSONResponse LogsResponse + +func (response GetLogs200JSONResponse) VisitGetLogsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetLogs401JSONResponse ErrorResponse + +func (response GetLogs401JSONResponse) VisitGetLogsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetLogs500JSONResponse ErrorResponse + +func (response GetLogs500JSONResponse) VisitGetLogsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetUsersRequestObject struct { +} + +type GetUsersResponseObject interface { + VisitGetUsersResponse(w http.ResponseWriter) error +} + +type GetUsers200JSONResponse UsersResponse + +func (response GetUsers200JSONResponse) VisitGetUsersResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetUsers401JSONResponse ErrorResponse + +func (response GetUsers401JSONResponse) VisitGetUsersResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetUsers500JSONResponse ErrorResponse + +func (response GetUsers500JSONResponse) VisitGetUsersResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateUserRequestObject struct { + Body *UpdateUserFormdataRequestBody +} + +type UpdateUserResponseObject interface { + VisitUpdateUserResponse(w http.ResponseWriter) error +} + +type UpdateUser200JSONResponse UsersResponse + +func (response UpdateUser200JSONResponse) VisitUpdateUserResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateUser400JSONResponse ErrorResponse + +func (response UpdateUser400JSONResponse) VisitUpdateUserResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateUser401JSONResponse ErrorResponse + +func (response UpdateUser401JSONResponse) VisitUpdateUserResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateUser500JSONResponse ErrorResponse + +func (response UpdateUser500JSONResponse) VisitUpdateUserResponse(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 Login200ResponseHeaders struct { + SetCookie string +} + +type Login200JSONResponse struct { + Body LoginResponse + Headers Login200ResponseHeaders +} + +func (response Login200JSONResponse) VisitLoginResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Set-Cookie", fmt.Sprint(response.Headers.SetCookie)) + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response.Body) +} + +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 RegisterRequestObject struct { + Body *RegisterJSONRequestBody +} + +type RegisterResponseObject interface { + VisitRegisterResponse(w http.ResponseWriter) error +} + +type Register201ResponseHeaders struct { + SetCookie string +} + +type Register201JSONResponse struct { + Body LoginResponse + Headers Register201ResponseHeaders +} + +func (response Register201JSONResponse) VisitRegisterResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Set-Cookie", fmt.Sprint(response.Headers.SetCookie)) + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response.Body) +} + +type Register400JSONResponse ErrorResponse + +func (response Register400JSONResponse) VisitRegisterResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type Register403JSONResponse ErrorResponse + +func (response Register403JSONResponse) VisitRegisterResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + + return json.NewEncoder(w).Encode(response) +} + +type Register500JSONResponse ErrorResponse + +func (response Register500JSONResponse) VisitRegisterResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + 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 CreateDocumentRequestObject struct { + Body *multipart.Reader +} + +type CreateDocumentResponseObject interface { + VisitCreateDocumentResponse(w http.ResponseWriter) error +} + +type CreateDocument200JSONResponse DocumentResponse + +func (response CreateDocument200JSONResponse) VisitCreateDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type CreateDocument400JSONResponse ErrorResponse + +func (response CreateDocument400JSONResponse) VisitCreateDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CreateDocument401JSONResponse ErrorResponse + +func (response CreateDocument401JSONResponse) VisitCreateDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type CreateDocument500JSONResponse ErrorResponse + +func (response CreateDocument500JSONResponse) VisitCreateDocumentResponse(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 EditDocumentRequestObject struct { + Id string `json:"id"` + Body *EditDocumentJSONRequestBody +} + +type EditDocumentResponseObject interface { + VisitEditDocumentResponse(w http.ResponseWriter) error +} + +type EditDocument200JSONResponse DocumentResponse + +func (response EditDocument200JSONResponse) VisitEditDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type EditDocument400JSONResponse ErrorResponse + +func (response EditDocument400JSONResponse) VisitEditDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type EditDocument401JSONResponse ErrorResponse + +func (response EditDocument401JSONResponse) VisitEditDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type EditDocument404JSONResponse ErrorResponse + +func (response EditDocument404JSONResponse) VisitEditDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type EditDocument500JSONResponse ErrorResponse + +func (response EditDocument500JSONResponse) VisitEditDocumentResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocumentCoverRequestObject struct { + Id string `json:"id"` +} + +type GetDocumentCoverResponseObject interface { + VisitGetDocumentCoverResponse(w http.ResponseWriter) error +} + +type GetDocumentCover200ImagejpegResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response GetDocumentCover200ImagejpegResponse) VisitGetDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "image/jpeg") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +type GetDocumentCover200ImagepngResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response GetDocumentCover200ImagepngResponse) VisitGetDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "image/png") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +type GetDocumentCover401JSONResponse ErrorResponse + +func (response GetDocumentCover401JSONResponse) VisitGetDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocumentCover404JSONResponse ErrorResponse + +func (response GetDocumentCover404JSONResponse) VisitGetDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocumentCover500JSONResponse ErrorResponse + +func (response GetDocumentCover500JSONResponse) VisitGetDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type UploadDocumentCoverRequestObject struct { + Id string `json:"id"` + Body *multipart.Reader +} + +type UploadDocumentCoverResponseObject interface { + VisitUploadDocumentCoverResponse(w http.ResponseWriter) error +} + +type UploadDocumentCover200JSONResponse DocumentResponse + +func (response UploadDocumentCover200JSONResponse) VisitUploadDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type UploadDocumentCover400JSONResponse ErrorResponse + +func (response UploadDocumentCover400JSONResponse) VisitUploadDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type UploadDocumentCover401JSONResponse ErrorResponse + +func (response UploadDocumentCover401JSONResponse) VisitUploadDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type UploadDocumentCover404JSONResponse ErrorResponse + +func (response UploadDocumentCover404JSONResponse) VisitUploadDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type UploadDocumentCover500JSONResponse ErrorResponse + +func (response UploadDocumentCover500JSONResponse) VisitUploadDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocumentFileRequestObject struct { + Id string `json:"id"` +} + +type GetDocumentFileResponseObject interface { + VisitGetDocumentFileResponse(w http.ResponseWriter) error +} + +type GetDocumentFile200ApplicationoctetStreamResponse struct { + Body io.Reader + ContentLength int64 +} + +func (response GetDocumentFile200ApplicationoctetStreamResponse) VisitGetDocumentFileResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/octet-stream") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +type GetDocumentFile401JSONResponse ErrorResponse + +func (response GetDocumentFile401JSONResponse) VisitGetDocumentFileResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocumentFile404JSONResponse ErrorResponse + +func (response GetDocumentFile404JSONResponse) VisitGetDocumentFileResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetDocumentFile500JSONResponse ErrorResponse + +func (response GetDocumentFile500JSONResponse) VisitGetDocumentFileResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetHomeRequestObject struct { +} + +type GetHomeResponseObject interface { + VisitGetHomeResponse(w http.ResponseWriter) error +} + +type GetHome200JSONResponse HomeResponse + +func (response GetHome200JSONResponse) VisitGetHomeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetHome401JSONResponse ErrorResponse + +func (response GetHome401JSONResponse) VisitGetHomeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetHome500JSONResponse ErrorResponse + +func (response GetHome500JSONResponse) VisitGetHomeResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetGraphDataRequestObject struct { +} + +type GetGraphDataResponseObject interface { + VisitGetGraphDataResponse(w http.ResponseWriter) error +} + +type GetGraphData200JSONResponse GraphDataResponse + +func (response GetGraphData200JSONResponse) VisitGetGraphDataResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetGraphData401JSONResponse ErrorResponse + +func (response GetGraphData401JSONResponse) VisitGetGraphDataResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetGraphData500JSONResponse ErrorResponse + +func (response GetGraphData500JSONResponse) VisitGetGraphDataResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetUserStatisticsRequestObject struct { +} + +type GetUserStatisticsResponseObject interface { + VisitGetUserStatisticsResponse(w http.ResponseWriter) error +} + +type GetUserStatistics200JSONResponse UserStatisticsResponse + +func (response GetUserStatistics200JSONResponse) VisitGetUserStatisticsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetUserStatistics401JSONResponse ErrorResponse + +func (response GetUserStatistics401JSONResponse) VisitGetUserStatisticsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetUserStatistics500JSONResponse ErrorResponse + +func (response GetUserStatistics500JSONResponse) VisitGetUserStatisticsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetStreaksRequestObject struct { +} + +type GetStreaksResponseObject interface { + VisitGetStreaksResponse(w http.ResponseWriter) error +} + +type GetStreaks200JSONResponse StreaksResponse + +func (response GetStreaks200JSONResponse) VisitGetStreaksResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetStreaks401JSONResponse ErrorResponse + +func (response GetStreaks401JSONResponse) VisitGetStreaksResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetStreaks500JSONResponse ErrorResponse + +func (response GetStreaks500JSONResponse) VisitGetStreaksResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetInfoRequestObject struct { +} + +type GetInfoResponseObject interface { + VisitGetInfoResponse(w http.ResponseWriter) error +} + +type GetInfo200JSONResponse InfoResponse + +func (response GetInfo200JSONResponse) VisitGetInfoResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetInfo500JSONResponse ErrorResponse + +func (response GetInfo500JSONResponse) VisitGetInfoResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetProgressListRequestObject struct { + Params GetProgressListParams +} + +type GetProgressListResponseObject interface { + VisitGetProgressListResponse(w http.ResponseWriter) error +} + +type GetProgressList200JSONResponse ProgressListResponse + +func (response GetProgressList200JSONResponse) VisitGetProgressListResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetProgressList401JSONResponse ErrorResponse + +func (response GetProgressList401JSONResponse) VisitGetProgressListResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetProgressList500JSONResponse ErrorResponse + +func (response GetProgressList500JSONResponse) VisitGetProgressListResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateProgressRequestObject struct { + Body *UpdateProgressJSONRequestBody +} + +type UpdateProgressResponseObject interface { + VisitUpdateProgressResponse(w http.ResponseWriter) error +} + +type UpdateProgress200JSONResponse UpdateProgressResponse + +func (response UpdateProgress200JSONResponse) VisitUpdateProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateProgress400JSONResponse ErrorResponse + +func (response UpdateProgress400JSONResponse) VisitUpdateProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateProgress401JSONResponse ErrorResponse + +func (response UpdateProgress401JSONResponse) VisitUpdateProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateProgress500JSONResponse ErrorResponse + +func (response UpdateProgress500JSONResponse) VisitUpdateProgressResponse(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 GetSearchRequestObject struct { + Params GetSearchParams +} + +type GetSearchResponseObject interface { + VisitGetSearchResponse(w http.ResponseWriter) error +} + +type GetSearch200JSONResponse SearchResponse + +func (response GetSearch200JSONResponse) VisitGetSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetSearch400JSONResponse ErrorResponse + +func (response GetSearch400JSONResponse) VisitGetSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type GetSearch401JSONResponse ErrorResponse + +func (response GetSearch401JSONResponse) VisitGetSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type GetSearch500JSONResponse ErrorResponse + +func (response GetSearch500JSONResponse) VisitGetSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type PostSearchRequestObject struct { + Body *PostSearchFormdataRequestBody +} + +type PostSearchResponseObject interface { + VisitPostSearchResponse(w http.ResponseWriter) error +} + +type PostSearch200Response struct { +} + +func (response PostSearch200Response) VisitPostSearchResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type PostSearch401JSONResponse ErrorResponse + +func (response PostSearch401JSONResponse) VisitPostSearchResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type PostSearch500JSONResponse ErrorResponse + +func (response PostSearch500JSONResponse) VisitPostSearchResponse(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) +} + +type UpdateSettingsRequestObject struct { + Body *UpdateSettingsJSONRequestBody +} + +type UpdateSettingsResponseObject interface { + VisitUpdateSettingsResponse(w http.ResponseWriter) error +} + +type UpdateSettings200JSONResponse SettingsResponse + +func (response UpdateSettings200JSONResponse) VisitUpdateSettingsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateSettings400JSONResponse ErrorResponse + +func (response UpdateSettings400JSONResponse) VisitUpdateSettingsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateSettings401JSONResponse ErrorResponse + +func (response UpdateSettings401JSONResponse) VisitUpdateSettingsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateSettings500JSONResponse ErrorResponse + +func (response UpdateSettings500JSONResponse) VisitUpdateSettingsResponse(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) + // Create activity records + // (POST /activity) + CreateActivity(ctx context.Context, request CreateActivityRequestObject) (CreateActivityResponseObject, error) + // Get admin page data + // (GET /admin) + GetAdmin(ctx context.Context, request GetAdminRequestObject) (GetAdminResponseObject, error) + // Perform admin action (backup, restore, etc.) + // (POST /admin) + PostAdminAction(ctx context.Context, request PostAdminActionRequestObject) (PostAdminActionResponseObject, error) + // Get import directory list + // (GET /admin/import) + GetImportDirectory(ctx context.Context, request GetImportDirectoryRequestObject) (GetImportDirectoryResponseObject, error) + // Perform import + // (POST /admin/import) + PostImport(ctx context.Context, request PostImportRequestObject) (PostImportResponseObject, error) + // Get import results + // (GET /admin/import-results) + GetImportResults(ctx context.Context, request GetImportResultsRequestObject) (GetImportResultsResponseObject, error) + // Get logs with optional filter + // (GET /admin/logs) + GetLogs(ctx context.Context, request GetLogsRequestObject) (GetLogsResponseObject, error) + // Get all users + // (GET /admin/users) + GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error) + // Create, update, or delete user + // (POST /admin/users) + UpdateUser(ctx context.Context, request UpdateUserRequestObject) (UpdateUserResponseObject, 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) + // User registration + // (POST /auth/register) + Register(ctx context.Context, request RegisterRequestObject) (RegisterResponseObject, error) + // List documents + // (GET /documents) + GetDocuments(ctx context.Context, request GetDocumentsRequestObject) (GetDocumentsResponseObject, error) + // Upload a new document + // (POST /documents) + CreateDocument(ctx context.Context, request CreateDocumentRequestObject) (CreateDocumentResponseObject, error) + // Get a single document + // (GET /documents/{id}) + GetDocument(ctx context.Context, request GetDocumentRequestObject) (GetDocumentResponseObject, error) + // Update document editable fields + // (POST /documents/{id}) + EditDocument(ctx context.Context, request EditDocumentRequestObject) (EditDocumentResponseObject, error) + // Get document cover image + // (GET /documents/{id}/cover) + GetDocumentCover(ctx context.Context, request GetDocumentCoverRequestObject) (GetDocumentCoverResponseObject, error) + // Upload document cover image + // (POST /documents/{id}/cover) + UploadDocumentCover(ctx context.Context, request UploadDocumentCoverRequestObject) (UploadDocumentCoverResponseObject, error) + // Download document file + // (GET /documents/{id}/file) + GetDocumentFile(ctx context.Context, request GetDocumentFileRequestObject) (GetDocumentFileResponseObject, error) + // Get home page data + // (GET /home) + GetHome(ctx context.Context, request GetHomeRequestObject) (GetHomeResponseObject, error) + // Get daily read stats graph data + // (GET /home/graph) + GetGraphData(ctx context.Context, request GetGraphDataRequestObject) (GetGraphDataResponseObject, error) + // Get user statistics (leaderboards) + // (GET /home/statistics) + GetUserStatistics(ctx context.Context, request GetUserStatisticsRequestObject) (GetUserStatisticsResponseObject, error) + // Get user streaks + // (GET /home/streaks) + GetStreaks(ctx context.Context, request GetStreaksRequestObject) (GetStreaksResponseObject, error) + // Get server information + // (GET /info) + GetInfo(ctx context.Context, request GetInfoRequestObject) (GetInfoResponseObject, error) + // List progress records + // (GET /progress) + GetProgressList(ctx context.Context, request GetProgressListRequestObject) (GetProgressListResponseObject, error) + // Update document progress + // (PUT /progress) + UpdateProgress(ctx context.Context, request UpdateProgressRequestObject) (UpdateProgressResponseObject, error) + // Get document progress + // (GET /progress/{id}) + GetProgress(ctx context.Context, request GetProgressRequestObject) (GetProgressResponseObject, error) + // Search external book sources + // (GET /search) + GetSearch(ctx context.Context, request GetSearchRequestObject) (GetSearchResponseObject, error) + // Download search result + // (POST /search) + PostSearch(ctx context.Context, request PostSearchRequestObject) (PostSearchResponseObject, error) + // Get user settings + // (GET /settings) + GetSettings(ctx context.Context, request GetSettingsRequestObject) (GetSettingsResponseObject, error) + // Update user settings + // (PUT /settings) + UpdateSettings(ctx context.Context, request UpdateSettingsRequestObject) (UpdateSettingsResponseObject, 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)) + } +} + +// CreateActivity operation middleware +func (sh *strictHandler) CreateActivity(w http.ResponseWriter, r *http.Request) { + var request CreateActivityRequestObject + + var body CreateActivityJSONRequestBody + 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.CreateActivity(ctx, request.(CreateActivityRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateActivity") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(CreateActivityResponseObject); ok { + if err := validResponse.VisitCreateActivityResponse(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)) + } +} + +// GetAdmin operation middleware +func (sh *strictHandler) GetAdmin(w http.ResponseWriter, r *http.Request) { + var request GetAdminRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetAdmin(ctx, request.(GetAdminRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetAdmin") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetAdminResponseObject); ok { + if err := validResponse.VisitGetAdminResponse(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)) + } +} + +// PostAdminAction operation middleware +func (sh *strictHandler) PostAdminAction(w http.ResponseWriter, r *http.Request) { + var request PostAdminActionRequestObject + + if reader, err := r.MultipartReader(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err)) + return + } else { + request.Body = reader + } + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostAdminAction(ctx, request.(PostAdminActionRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostAdminAction") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostAdminActionResponseObject); ok { + if err := validResponse.VisitPostAdminActionResponse(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)) + } +} + +// GetImportDirectory operation middleware +func (sh *strictHandler) GetImportDirectory(w http.ResponseWriter, r *http.Request, params GetImportDirectoryParams) { + var request GetImportDirectoryRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetImportDirectory(ctx, request.(GetImportDirectoryRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetImportDirectory") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetImportDirectoryResponseObject); ok { + if err := validResponse.VisitGetImportDirectoryResponse(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)) + } +} + +// PostImport operation middleware +func (sh *strictHandler) PostImport(w http.ResponseWriter, r *http.Request) { + var request PostImportRequestObject + + if err := r.ParseForm(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode formdata: %w", err)) + return + } + var body PostImportFormdataRequestBody + if err := runtime.BindForm(&body, r.Form, nil, nil); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't bind formdata: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostImport(ctx, request.(PostImportRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostImport") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostImportResponseObject); ok { + if err := validResponse.VisitPostImportResponse(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)) + } +} + +// GetImportResults operation middleware +func (sh *strictHandler) GetImportResults(w http.ResponseWriter, r *http.Request) { + var request GetImportResultsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetImportResults(ctx, request.(GetImportResultsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetImportResults") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetImportResultsResponseObject); ok { + if err := validResponse.VisitGetImportResultsResponse(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)) + } +} + +// GetLogs operation middleware +func (sh *strictHandler) GetLogs(w http.ResponseWriter, r *http.Request, params GetLogsParams) { + var request GetLogsRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetLogs(ctx, request.(GetLogsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetLogs") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetLogsResponseObject); ok { + if err := validResponse.VisitGetLogsResponse(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)) + } +} + +// GetUsers operation middleware +func (sh *strictHandler) GetUsers(w http.ResponseWriter, r *http.Request) { + var request GetUsersRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetUsers(ctx, request.(GetUsersRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetUsers") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetUsersResponseObject); ok { + if err := validResponse.VisitGetUsersResponse(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)) + } +} + +// UpdateUser operation middleware +func (sh *strictHandler) UpdateUser(w http.ResponseWriter, r *http.Request) { + var request UpdateUserRequestObject + + if err := r.ParseForm(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode formdata: %w", err)) + return + } + var body UpdateUserFormdataRequestBody + if err := runtime.BindForm(&body, r.Form, nil, nil); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't bind formdata: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.UpdateUser(ctx, request.(UpdateUserRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateUser") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UpdateUserResponseObject); ok { + if err := validResponse.VisitUpdateUserResponse(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)) + } +} + +// Register operation middleware +func (sh *strictHandler) Register(w http.ResponseWriter, r *http.Request) { + var request RegisterRequestObject + + var body RegisterJSONRequestBody + 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.Register(ctx, request.(RegisterRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "Register") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(RegisterResponseObject); ok { + if err := validResponse.VisitRegisterResponse(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)) + } +} + +// CreateDocument operation middleware +func (sh *strictHandler) CreateDocument(w http.ResponseWriter, r *http.Request) { + var request CreateDocumentRequestObject + + if reader, err := r.MultipartReader(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err)) + return + } else { + request.Body = reader + } + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.CreateDocument(ctx, request.(CreateDocumentRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateDocument") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(CreateDocumentResponseObject); ok { + if err := validResponse.VisitCreateDocumentResponse(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)) + } +} + +// EditDocument operation middleware +func (sh *strictHandler) EditDocument(w http.ResponseWriter, r *http.Request, id string) { + var request EditDocumentRequestObject + + request.Id = id + + var body EditDocumentJSONRequestBody + 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.EditDocument(ctx, request.(EditDocumentRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "EditDocument") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(EditDocumentResponseObject); ok { + if err := validResponse.VisitEditDocumentResponse(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)) + } +} + +// GetDocumentCover operation middleware +func (sh *strictHandler) GetDocumentCover(w http.ResponseWriter, r *http.Request, id string) { + var request GetDocumentCoverRequestObject + + request.Id = id + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDocumentCover(ctx, request.(GetDocumentCoverRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDocumentCover") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDocumentCoverResponseObject); ok { + if err := validResponse.VisitGetDocumentCoverResponse(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)) + } +} + +// UploadDocumentCover operation middleware +func (sh *strictHandler) UploadDocumentCover(w http.ResponseWriter, r *http.Request, id string) { + var request UploadDocumentCoverRequestObject + + request.Id = id + + if reader, err := r.MultipartReader(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err)) + return + } else { + request.Body = reader + } + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.UploadDocumentCover(ctx, request.(UploadDocumentCoverRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UploadDocumentCover") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UploadDocumentCoverResponseObject); ok { + if err := validResponse.VisitUploadDocumentCoverResponse(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)) + } +} + +// GetDocumentFile operation middleware +func (sh *strictHandler) GetDocumentFile(w http.ResponseWriter, r *http.Request, id string) { + var request GetDocumentFileRequestObject + + request.Id = id + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDocumentFile(ctx, request.(GetDocumentFileRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDocumentFile") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDocumentFileResponseObject); ok { + if err := validResponse.VisitGetDocumentFileResponse(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)) + } +} + +// GetHome operation middleware +func (sh *strictHandler) GetHome(w http.ResponseWriter, r *http.Request) { + var request GetHomeRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetHome(ctx, request.(GetHomeRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetHome") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetHomeResponseObject); ok { + if err := validResponse.VisitGetHomeResponse(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)) + } +} + +// GetGraphData operation middleware +func (sh *strictHandler) GetGraphData(w http.ResponseWriter, r *http.Request) { + var request GetGraphDataRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetGraphData(ctx, request.(GetGraphDataRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetGraphData") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetGraphDataResponseObject); ok { + if err := validResponse.VisitGetGraphDataResponse(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)) + } +} + +// GetUserStatistics operation middleware +func (sh *strictHandler) GetUserStatistics(w http.ResponseWriter, r *http.Request) { + var request GetUserStatisticsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetUserStatistics(ctx, request.(GetUserStatisticsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetUserStatistics") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetUserStatisticsResponseObject); ok { + if err := validResponse.VisitGetUserStatisticsResponse(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)) + } +} + +// GetStreaks operation middleware +func (sh *strictHandler) GetStreaks(w http.ResponseWriter, r *http.Request) { + var request GetStreaksRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetStreaks(ctx, request.(GetStreaksRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetStreaks") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetStreaksResponseObject); ok { + if err := validResponse.VisitGetStreaksResponse(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)) + } +} + +// GetInfo operation middleware +func (sh *strictHandler) GetInfo(w http.ResponseWriter, r *http.Request) { + var request GetInfoRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetInfo(ctx, request.(GetInfoRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetInfo") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetInfoResponseObject); ok { + if err := validResponse.VisitGetInfoResponse(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)) + } +} + +// GetProgressList operation middleware +func (sh *strictHandler) GetProgressList(w http.ResponseWriter, r *http.Request, params GetProgressListParams) { + var request GetProgressListRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetProgressList(ctx, request.(GetProgressListRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetProgressList") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetProgressListResponseObject); ok { + if err := validResponse.VisitGetProgressListResponse(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)) + } +} + +// UpdateProgress operation middleware +func (sh *strictHandler) UpdateProgress(w http.ResponseWriter, r *http.Request) { + var request UpdateProgressRequestObject + + var body UpdateProgressJSONRequestBody + 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.UpdateProgress(ctx, request.(UpdateProgressRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateProgress") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UpdateProgressResponseObject); ok { + if err := validResponse.VisitUpdateProgressResponse(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)) + } +} + +// GetSearch operation middleware +func (sh *strictHandler) GetSearch(w http.ResponseWriter, r *http.Request, params GetSearchParams) { + var request GetSearchRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetSearch(ctx, request.(GetSearchRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetSearch") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetSearchResponseObject); ok { + if err := validResponse.VisitGetSearchResponse(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)) + } +} + +// PostSearch operation middleware +func (sh *strictHandler) PostSearch(w http.ResponseWriter, r *http.Request) { + var request PostSearchRequestObject + + if err := r.ParseForm(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode formdata: %w", err)) + return + } + var body PostSearchFormdataRequestBody + if err := runtime.BindForm(&body, r.Form, nil, nil); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't bind formdata: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PostSearch(ctx, request.(PostSearchRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PostSearch") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PostSearchResponseObject); ok { + if err := validResponse.VisitPostSearchResponse(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)) + } +} + +// UpdateSettings operation middleware +func (sh *strictHandler) UpdateSettings(w http.ResponseWriter, r *http.Request) { + var request UpdateSettingsRequestObject + + var body UpdateSettingsJSONRequestBody + 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.UpdateSettings(ctx, request.(UpdateSettingsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateSettings") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UpdateSettingsResponseObject); ok { + if err := validResponse.VisitUpdateSettingsResponse(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 new file mode 100644 index 0000000..ca824a8 --- /dev/null +++ b/api/v1/auth.go @@ -0,0 +1,286 @@ +package v1 + +import ( + "context" + "crypto/md5" + "fmt" + "net/http" + "time" + + argon2 "github.com/alexedwards/argon2id" + "github.com/gorilla/sessions" + log "github.com/sirupsen/logrus" +) + +// 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 + } + + if err := s.saveUserSession(ctx, user.ID, user.Admin, *user.AuthHash); err != nil { + return Login500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + return Login200JSONResponse{ + Body: LoginResponse{ + Username: user.ID, + IsAdmin: user.Admin, + }, + Headers: Login200ResponseHeaders{ + SetCookie: s.getSetCookieFromContext(ctx), + }, + }, nil +} + +// POST /auth/register +func (s *Server) Register(ctx context.Context, request RegisterRequestObject) (RegisterResponseObject, error) { + if !s.cfg.RegistrationEnabled { + return Register403JSONResponse{Code: 403, Message: "Registration is disabled"}, nil + } + + if request.Body == nil { + return Register400JSONResponse{Code: 400, Message: "Invalid request body"}, nil + } + + req := *request.Body + if req.Username == "" || req.Password == "" { + return Register400JSONResponse{Code: 400, Message: "Invalid user or password"}, nil + } + + currentUsers, err := s.db.Queries.GetUsers(ctx) + if err != nil { + return Register500JSONResponse{Code: 500, Message: "Failed to create user"}, nil + } + + isAdmin := len(currentUsers) == 0 + if err := s.createUser(ctx, req.Username, &req.Password, &isAdmin); err != nil { + return Register400JSONResponse{Code: 400, Message: err.Error()}, nil + } + + user, err := s.db.Queries.GetUser(ctx, req.Username) + if err != nil { + return Register500JSONResponse{Code: 500, Message: "Failed to load created user"}, nil + } + + if err := s.saveUserSession(ctx, user.ID, user.Admin, *user.AuthHash); err != nil { + return Register500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + return Register201JSONResponse{ + Body: LoginResponse{ + Username: user.ID, + IsAdmin: user.Admin, + }, + Headers: Register201ResponseHeaders{ + SetCookie: s.getSetCookieFromContext(ctx), + }, + }, nil +} + +// 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 + } + + session, err := s.getCookieSession(r) + if err != nil { + return Logout401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + 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 +} + +func (s *Server) saveUserSession(ctx context.Context, username string, isAdmin bool, authHash string) error { + r := s.getRequestFromContext(ctx) + w := s.getResponseWriterFromContext(ctx) + if r == nil || w == nil { + return fmt.Errorf("internal context error") + } + + session, err := s.getCookieSession(r) + if err != nil { + return fmt.Errorf("unauthorized") + } + + session.Values["authorizedUser"] = username + session.Values["isAdmin"] = isAdmin + session.Values["expiresAt"] = time.Now().Unix() + (60 * 60 * 24 * 7) + session.Values["authHash"] = authHash + + if err := session.Save(r, w); err != nil { + return fmt.Errorf("failed to create session") + } + + return nil +} + +func (s *Server) getCookieSession(r *http.Request) (*sessions.Session, error) { + 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, err := store.Get(r, "token") + if err != nil { + return nil, fmt.Errorf("failed to get session: %w", err) + } + + session.Options.SameSite = http.SameSiteLaxMode + session.Options.HttpOnly = true + session.Options.Secure = s.cfg.CookieSecure + + return session, 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 +} + +// isAdmin checks if a user has admin privileges +func (s *Server) isAdmin(ctx context.Context) bool { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return false + } + return auth.IsAdmin +} + +// 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 +} + +func (s *Server) getSetCookieFromContext(ctx context.Context) string { + w := s.getResponseWriterFromContext(ctx) + if w == nil { + return "" + } + return w.Header().Get("Set-Cookie") +} + +// getSession retrieves auth data from the session cookie +func (s *Server) getSession(r *http.Request) (auth authData, ok bool) { + // Get session from cookie store + 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)) + } else { + log.Error("invalid cookie encryption key (must be 16 or 32 bytes)") + return authData{}, false + } + } + + session, err := store.Get(r, "token") + if err != nil { + return authData{}, false + } + + // Get session values + authorizedUser := session.Values["authorizedUser"] + isAdmin := session.Values["isAdmin"] + expiresAt := session.Values["expiresAt"] + authHash := session.Values["authHash"] + + if authorizedUser == nil || isAdmin == nil || expiresAt == nil || authHash == nil { + return authData{}, false + } + + auth = authData{ + UserName: authorizedUser.(string), + IsAdmin: isAdmin.(bool), + AuthHash: authHash.(string), + } + + // Validate auth hash + ctx := r.Context() + correctAuthHash, err := s.getUserAuthHash(ctx, auth.UserName) + if err != nil || correctAuthHash != auth.AuthHash { + return authData{}, false + } + + return auth, true +} + +// getUserAuthHash retrieves the user's auth hash from DB or cache +func (s *Server) getUserAuthHash(ctx context.Context, username string) (string, error) { + user, err := s.db.Queries.GetUser(ctx, username) + if err != nil { + return "", err + } + return *user.AuthHash, nil +} + +// authData represents authenticated user information +type authData struct { + UserName string + IsAdmin bool + AuthHash string +} diff --git a/api/v1/auth_test.go b/api/v1/auth_test.go new file mode 100644 index 0000000..fc3fcfe --- /dev/null +++ b/api/v1/auth_test.go @@ -0,0 +1,228 @@ +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" +) + +type AuthTestSuite struct { + suite.Suite + db *database.DBManager + cfg *config.Config + srv *Server +} + +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, nil) +} + +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) assertSessionCookie(cookie *http.Cookie) { + suite.Require().NotNil(cookie) + suite.Equal("token", cookie.Name) + suite.NotEmpty(cookie.Value) + suite.True(cookie.HttpOnly) +} + +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") + suite.assertSessionCookie(cookies[0]) + + return cookies[0] +} + +func (suite *AuthTestSuite) TestAPILogin() { + suite.createTestUser("testuser", "testpass") + + reqBody := LoginRequest{ + Username: "testuser", + Password: "testpass", + } + body, _ := json.Marshal(reqBody) + + 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) + + var resp LoginResponse + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal("testuser", resp.Username) + + cookies := w.Result().Cookies() + suite.Require().Len(cookies, 1) + suite.assertSessionCookie(cookies[0]) +} + +func (suite *AuthTestSuite) TestAPILoginInvalidCredentials() { + reqBody := LoginRequest{ + Username: "testuser", + Password: "wrongpass", + } + body, _ := json.Marshal(reqBody) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusUnauthorized, w.Code) +} + +func (suite *AuthTestSuite) TestAPIRegister() { + reqBody := LoginRequest{ + Username: "newuser", + Password: "newpass", + } + body, _ := json.Marshal(reqBody) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(body)) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusCreated, w.Code) + + var resp LoginResponse + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal("newuser", resp.Username) + suite.True(resp.IsAdmin, "first registered user should mirror legacy admin bootstrap behavior") + + cookies := w.Result().Cookies() + suite.Require().Len(cookies, 1, "register should set a session cookie") + suite.assertSessionCookie(cookies[0]) + + user, err := suite.db.Queries.GetUser(suite.T().Context(), "newuser") + suite.Require().NoError(err) + suite.True(user.Admin) +} + +func (suite *AuthTestSuite) TestAPIRegisterDisabled() { + suite.cfg.RegistrationEnabled = false + suite.srv = NewServer(suite.db, suite.cfg, nil) + + reqBody := LoginRequest{ + Username: "newuser", + Password: "newpass", + } + body, _ := json.Marshal(reqBody) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(body)) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusForbidden, w.Code) +} + +func (suite *AuthTestSuite) TestAPILogout() { + suite.createTestUser("testuser", "testpass") + cookie := suite.login("testuser", "testpass") + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/logout", nil) + req.AddCookie(cookie) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusOK, w.Code) + + cookies := w.Result().Cookies() + suite.Require().Len(cookies, 1) + suite.Equal("token", cookies[0].Name) +} + +func (suite *AuthTestSuite) TestAPIGetMe() { + suite.createTestUser("testuser", "testpass") + cookie := suite.login("testuser", "testpass") + + req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil) + req.AddCookie(cookie) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusOK, w.Code) + + var resp UserData + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal("testuser", resp.Username) +} + +func (suite *AuthTestSuite) TestAPIGetMeUnauthenticated() { + req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusUnauthorized, w.Code) +} diff --git a/api/v1/documents.go b/api/v1/documents.go new file mode 100644 index 0000000..242ed6e --- /dev/null +++ b/api/v1/documents.go @@ -0,0 +1,827 @@ +package v1 + +import ( + "context" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "reichard.io/antholume/database" + "reichard.io/antholume/metadata" +) + +// GET /documents +func (s *Server) GetDocuments(ctx context.Context, request GetDocumentsRequestObject) (GetDocumentsResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetDocuments401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + 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 + "%" + } + + rows, err := s.db.Queries.GetDocumentsWithStats( + ctx, + database.GetDocumentsWithStatsParams{ + UserID: auth.UserName, + Query: &search, + Deleted: ptrOf(false), + Offset: (page - 1) * limit, + Limit: limit, + }, + ) + if err != nil { + return GetDocuments500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + total := int64(len(rows)) + var nextPage *int64 + var previousPage *int64 + if page*limit < total { + nextPage = ptrOf(page + 1) + } + if page > 1 { + previousPage = ptrOf(page - 1) + } + + apiDocuments := make([]Document, len(rows)) + for i, row := range rows { + apiDocuments[i] = Document{ + Id: row.ID, + Title: *row.Title, + Author: *row.Author, + Description: row.Description, + Isbn10: row.Isbn10, + Isbn13: row.Isbn13, + Words: row.Words, + Filepath: row.Filepath, + Percentage: ptrOf(float32(row.Percentage)), + TotalTimeSeconds: ptrOf(row.TotalTimeSeconds), + Wpm: ptrOf(float32(row.Wpm)), + SecondsPerPercent: ptrOf(row.SecondsPerPercent), + LastRead: parseInterfaceTime(row.LastRead), + CreatedAt: time.Now(), // Will be overwritten if we had a proper created_at from DB + UpdatedAt: time.Now(), // Will be overwritten if we had a proper updated_at from DB + Deleted: false, // Default, should be overridden if available + } + } + + response := DocumentsResponse{ + Documents: apiDocuments, + Total: total, + Page: page, + Limit: limit, + NextPage: nextPage, + PreviousPage: previousPage, + Search: request.Params.Search, + } + return GetDocuments200JSONResponse(response), nil +} + +// GET /documents/{id} +func (s *Server) GetDocument(ctx context.Context, request GetDocumentRequestObject) (GetDocumentResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetDocument401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + // Use GetDocumentsWithStats to get document with stats + docs, err := s.db.Queries.GetDocumentsWithStats( + ctx, + database.GetDocumentsWithStatsParams{ + UserID: auth.UserName, + ID: &request.Id, + Deleted: ptrOf(false), + Offset: 0, + Limit: 1, + }, + ) + if err != nil || len(docs) == 0 { + return GetDocument404JSONResponse{Code: 404, Message: "Document not found"}, nil + } + + doc := docs[0] + + apiDoc := Document{ + Id: doc.ID, + Title: *doc.Title, + Author: *doc.Author, + Description: doc.Description, + Isbn10: doc.Isbn10, + Isbn13: doc.Isbn13, + Words: doc.Words, + Filepath: doc.Filepath, + Percentage: ptrOf(float32(doc.Percentage)), + TotalTimeSeconds: ptrOf(doc.TotalTimeSeconds), + Wpm: ptrOf(float32(doc.Wpm)), + SecondsPerPercent: ptrOf(doc.SecondsPerPercent), + LastRead: parseInterfaceTime(doc.LastRead), + CreatedAt: time.Now(), // Will be overwritten if we had a proper created_at from DB + UpdatedAt: time.Now(), // Will be overwritten if we had a proper updated_at from DB + Deleted: false, // Default, should be overridden if available + } + + response := DocumentResponse{ + Document: apiDoc, + } + return GetDocument200JSONResponse(response), nil +} + +// POST /documents/{id} +func (s *Server) EditDocument(ctx context.Context, request EditDocumentRequestObject) (EditDocumentResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return EditDocument401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return EditDocument400JSONResponse{Code: 400, Message: "Missing request body"}, nil + } + + // Validate document exists and get current state + currentDoc, err := s.db.Queries.GetDocument(ctx, request.Id) + if err != nil { + return EditDocument404JSONResponse{Code: 404, Message: "Document not found"}, nil + } + + // Validate at least one editable field is provided + if request.Body.Title == nil && + request.Body.Author == nil && + request.Body.Description == nil && + request.Body.Isbn10 == nil && + request.Body.Isbn13 == nil && + request.Body.CoverGbid == nil { + return EditDocument400JSONResponse{Code: 400, Message: "No editable fields provided"}, nil + } + + // Handle cover via Google Books ID + var coverFileName *string + if request.Body.CoverGbid != nil { + coverDir := filepath.Join(s.cfg.DataPath, "covers") + fileName, err := metadata.CacheCoverWithContext(ctx, *request.Body.CoverGbid, coverDir, request.Id, true) + if err == nil { + coverFileName = fileName + } + } + + // Update document with provided editable fields only + _, err = s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ + ID: request.Id, + Title: request.Body.Title, + Author: request.Body.Author, + Description: request.Body.Description, + Isbn10: request.Body.Isbn10, + Isbn13: request.Body.Isbn13, + Coverfile: coverFileName, + // Preserve existing values for non-editable fields + Md5: currentDoc.Md5, + Basepath: currentDoc.Basepath, + Filepath: currentDoc.Filepath, + Words: currentDoc.Words, + }) + if err != nil { + log.Error("UpsertDocument DB Error:", err) + return EditDocument500JSONResponse{Code: 500, Message: "Failed to update document"}, nil + } + + // Use GetDocumentsWithStats to get document with stats for the response + docs, err := s.db.Queries.GetDocumentsWithStats( + ctx, + database.GetDocumentsWithStatsParams{ + UserID: auth.UserName, + ID: &request.Id, + Deleted: ptrOf(false), + Offset: 0, + Limit: 1, + }, + ) + if err != nil || len(docs) == 0 { + return EditDocument404JSONResponse{Code: 404, Message: "Document not found"}, nil + } + + doc := docs[0] + + + apiDoc := Document{ + Id: doc.ID, + Title: *doc.Title, + Author: *doc.Author, + Description: doc.Description, + Isbn10: doc.Isbn10, + Isbn13: doc.Isbn13, + Words: doc.Words, + Filepath: doc.Filepath, + Percentage: ptrOf(float32(doc.Percentage)), + TotalTimeSeconds: ptrOf(doc.TotalTimeSeconds), + Wpm: ptrOf(float32(doc.Wpm)), + SecondsPerPercent: ptrOf(doc.SecondsPerPercent), + LastRead: parseInterfaceTime(doc.LastRead), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Deleted: false, + } + + response := DocumentResponse{ + Document: apiDoc, + } + return EditDocument200JSONResponse(response), nil +} + +// deriveBaseFileName builds the base filename for a given MetadataInfo object. +func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string { + // Derive New FileName + var newFileName string + if metadataInfo.Author != nil && *metadataInfo.Author != "" { + newFileName = newFileName + *metadataInfo.Author + } else { + newFileName = newFileName + "Unknown" + } + if metadataInfo.Title != nil && *metadataInfo.Title != "" { + newFileName = newFileName + " - " + *metadataInfo.Title + } else { + newFileName = newFileName + " - Unknown" + } + + // Remove Slashes + fileName := strings.ReplaceAll(newFileName, "/", "") + return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type)) +} + +// parseInterfaceTime converts an interface{} to time.Time for SQLC queries +func parseInterfaceTime(t any) *time.Time { + if t == nil { + return nil + } + switch v := t.(type) { + case string: + parsed, err := time.Parse(time.RFC3339, v) + if err != nil { + return nil + } + return &parsed + case time.Time: + return &v + default: + return nil + } +} + +// serveNoCover serves the default no-cover image from assets +func (s *Server) serveNoCover() (fs.File, string, int64, error) { + // Try to open the no-cover image from assets + file, err := s.assets.Open("assets/images/no-cover.jpg") + if err != nil { + return nil, "", 0, err + } + + // Get file info + info, err := file.Stat() + if err != nil { + file.Close() + return nil, "", 0, err + } + + return file, "image/jpeg", info.Size(), nil +} + +// openFileReader opens a file and returns it as an io.ReaderCloser +func openFileReader(path string) (*os.File, error) { + return os.Open(path) +} + +// GET /documents/{id}/cover +func (s *Server) GetDocumentCover(ctx context.Context, request GetDocumentCoverRequestObject) (GetDocumentCoverResponseObject, error) { + // Authentication is handled by middleware, which also adds auth data to context + // This endpoint just serves the cover image + + // Validate Document Exists in DB + document, err := s.db.Queries.GetDocument(ctx, request.Id) + if err != nil { + log.Error("GetDocument DB Error:", err) + return GetDocumentCover404JSONResponse{Code: 404, Message: "Document not found"}, nil + } + + var coverFile fs.File + var contentType string + var contentLength int64 + var needMetadataFetch bool + + // Handle Identified Document + if document.Coverfile != nil { + if *document.Coverfile == "UNKNOWN" { + // Serve no-cover image + file, ct, size, err := s.serveNoCover() + if err != nil { + log.Error("Failed to open no-cover image:", err) + return GetDocumentCover404JSONResponse{Code: 404, Message: "Cover not found"}, nil + } + coverFile = file + contentType = ct + contentLength = size + needMetadataFetch = true + } else { + // Derive Path + coverPath := filepath.Join(s.cfg.DataPath, "covers", *document.Coverfile) + + // Validate File Exists + fileInfo, err := os.Stat(coverPath) + if os.IsNotExist(err) { + log.Error("Cover file should but doesn't exist: ", err) + // Serve no-cover image + file, ct, size, err := s.serveNoCover() + if err != nil { + log.Error("Failed to open no-cover image:", err) + return GetDocumentCover404JSONResponse{Code: 404, Message: "Cover not found"}, nil + } + coverFile = file + contentType = ct + contentLength = size + needMetadataFetch = true + } else { + // Open the cover file + file, err := openFileReader(coverPath) + if err != nil { + log.Error("Failed to open cover file:", err) + return GetDocumentCover500JSONResponse{Code: 500, Message: "Failed to open cover"}, nil + } + coverFile = file + contentLength = fileInfo.Size() + + // Determine content type based on file extension + contentType = "image/jpeg" + if strings.HasSuffix(coverPath, ".png") { + contentType = "image/png" + } + } + } + } else { + needMetadataFetch = true + } + + // Attempt Metadata fetch if needed + var cachedCoverFile string = "UNKNOWN" + var coverDir string = filepath.Join(s.cfg.DataPath, "covers") + + if needMetadataFetch { + // Create context with timeout for metadata service calls + metadataCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + // Identify Documents & Save Covers + metadataResults, err := metadata.SearchMetadataWithContext(metadataCtx, metadata.SOURCE_GBOOK, metadata.MetadataInfo{ + Title: document.Title, + Author: document.Author, + }) + + if err == nil && len(metadataResults) > 0 && metadataResults[0].ID != nil { + firstResult := metadataResults[0] + + // Save Cover + fileName, err := metadata.CacheCoverWithContext(metadataCtx, *firstResult.ID, coverDir, document.ID, false) + if err == nil { + cachedCoverFile = *fileName + } + + // Store First Metadata Result + if _, err = s.db.Queries.AddMetadata(ctx, database.AddMetadataParams{ + DocumentID: document.ID, + Title: firstResult.Title, + Author: firstResult.Author, + Description: firstResult.Description, + Gbid: firstResult.ID, + Olid: nil, + Isbn10: firstResult.ISBN10, + Isbn13: firstResult.ISBN13, + }); err != nil { + log.Error("AddMetadata DB Error:", err) + } + } + + // Upsert Document + if _, err = s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ + ID: document.ID, + Coverfile: &cachedCoverFile, + }); err != nil { + log.Warn("UpsertDocument DB Error:", err) + } + + // Update cover file if we got a new cover + if cachedCoverFile != "UNKNOWN" { + coverPath := filepath.Join(coverDir, cachedCoverFile) + fileInfo, err := os.Stat(coverPath) + if err != nil { + log.Error("Failed to stat cached cover:", err) + // Keep the no-cover image + } else { + file, err := openFileReader(coverPath) + if err != nil { + log.Error("Failed to open cached cover:", err) + // Keep the no-cover image + } else { + _ = coverFile.Close() // Close the previous file + coverFile = file + contentLength = fileInfo.Size() + + // Determine content type based on file extension + contentType = "image/jpeg" + if strings.HasSuffix(coverPath, ".png") { + contentType = "image/png" + } + } + } + } + } + + return &GetDocumentCover200Response{ + Body: coverFile, + ContentLength: contentLength, + ContentType: contentType, + }, nil +} + +// POST /documents/{id}/cover +func (s *Server) UploadDocumentCover(ctx context.Context, request UploadDocumentCoverRequestObject) (UploadDocumentCoverResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return UploadDocumentCover401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return UploadDocumentCover400JSONResponse{Code: 400, Message: "Missing request body"}, nil + } + + // Validate document exists + _, err := s.db.Queries.GetDocument(ctx, request.Id) + if err != nil { + return UploadDocumentCover404JSONResponse{Code: 404, Message: "Document not found"}, nil + } + + // Read multipart form + form, err := request.Body.ReadForm(32 << 20) // 32MB max + if err != nil { + log.Error("ReadForm error:", err) + return UploadDocumentCover500JSONResponse{Code: 500, Message: "Failed to read form"}, nil + } + + // Get file from form + fileField := form.File["cover_file"] + if len(fileField) == 0 { + return UploadDocumentCover400JSONResponse{Code: 400, Message: "No file provided"}, nil + } + + file := fileField[0] + + // Validate file extension + if !strings.HasSuffix(strings.ToLower(file.Filename), ".jpg") && !strings.HasSuffix(strings.ToLower(file.Filename), ".png") { + return UploadDocumentCover400JSONResponse{Code: 400, Message: "Only JPG and PNG files are allowed"}, nil + } + + // Open file + f, err := file.Open() + if err != nil { + log.Error("Open file error:", err) + return UploadDocumentCover500JSONResponse{Code: 500, Message: "Failed to open file"}, nil + } + defer f.Close() + + // Read file content + data, err := io.ReadAll(f) + if err != nil { + log.Error("Read file error:", err) + return UploadDocumentCover500JSONResponse{Code: 500, Message: "Failed to read file"}, nil + } + + // Validate actual content type + contentType := http.DetectContentType(data) + allowedTypes := map[string]bool{ + "image/jpeg": true, + "image/png": true, + } + if !allowedTypes[contentType] { + return UploadDocumentCover400JSONResponse{ + Code: 400, + Message: fmt.Sprintf("Invalid file type: %s. Only JPG and PNG files are allowed.", contentType), + }, nil + } + + // Derive storage path + coverDir := filepath.Join(s.cfg.DataPath, "covers") + fileName := fmt.Sprintf("%s%s", request.Id, strings.ToLower(filepath.Ext(file.Filename))) + safePath := filepath.Join(coverDir, fileName) + + // Save file + err = os.WriteFile(safePath, data, 0644) + if err != nil { + log.Error("Save file error:", err) + return UploadDocumentCover500JSONResponse{Code: 500, Message: "Unable to save cover"}, nil + } + + // Upsert document with new cover + _, err = s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ + ID: request.Id, + Coverfile: &fileName, + }) + if err != nil { + log.Error("UpsertDocument DB error:", err) + return UploadDocumentCover500JSONResponse{Code: 500, Message: "Failed to save cover"}, nil + } + + // Use GetDocumentsWithStats to get document with stats for the response + docs, err := s.db.Queries.GetDocumentsWithStats( + ctx, + database.GetDocumentsWithStatsParams{ + UserID: auth.UserName, + ID: &request.Id, + Deleted: ptrOf(false), + Offset: 0, + Limit: 1, + }, + ) + if err != nil || len(docs) == 0 { + return UploadDocumentCover404JSONResponse{Code: 404, Message: "Document not found"}, nil + } + + doc := docs[0] + + + apiDoc := Document{ + Id: doc.ID, + Title: *doc.Title, + Author: *doc.Author, + Description: doc.Description, + Isbn10: doc.Isbn10, + Isbn13: doc.Isbn13, + Words: doc.Words, + Filepath: doc.Filepath, + Percentage: ptrOf(float32(doc.Percentage)), + TotalTimeSeconds: ptrOf(doc.TotalTimeSeconds), + Wpm: ptrOf(float32(doc.Wpm)), + SecondsPerPercent: ptrOf(doc.SecondsPerPercent), + LastRead: parseInterfaceTime(doc.LastRead), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Deleted: false, + } + + response := DocumentResponse{ + Document: apiDoc, + } + return UploadDocumentCover200JSONResponse(response), nil +} + +// GET /documents/{id}/file +func (s *Server) GetDocumentFile(ctx context.Context, request GetDocumentFileRequestObject) (GetDocumentFileResponseObject, error) { + // Authentication is handled by middleware, which also adds auth data to context + // This endpoint just serves the document file download + // Get Document + document, err := s.db.Queries.GetDocument(ctx, request.Id) + if err != nil { + log.Error("GetDocument DB Error:", err) + return GetDocumentFile404JSONResponse{Code: 404, Message: "Document not found"}, nil + } + + if document.Filepath == nil { + log.Error("Document Doesn't Have File:", request.Id) + return GetDocumentFile404JSONResponse{Code: 404, Message: "Document file not found"}, nil + } + + // Derive Basepath + basepath := filepath.Join(s.cfg.DataPath, "documents") + if document.Basepath != nil && *document.Basepath != "" { + basepath = *document.Basepath + } + + // Derive Storage Location + filePath := filepath.Join(basepath, *document.Filepath) + + // Validate File Exists + fileInfo, err := os.Stat(filePath) + if os.IsNotExist(err) { + log.Error("File should but doesn't exist:", err) + return GetDocumentFile404JSONResponse{Code: 404, Message: "Document file not found"}, nil + } + + // Open file + file, err := os.Open(filePath) + if err != nil { + log.Error("Failed to open document file:", err) + return GetDocumentFile500JSONResponse{Code: 500, Message: "Failed to open document"}, nil + } + + return &GetDocumentFile200Response{ + Body: file, + ContentLength: fileInfo.Size(), + Filename: filepath.Base(*document.Filepath), + }, nil +} + +// POST /documents +func (s *Server) CreateDocument(ctx context.Context, request CreateDocumentRequestObject) (CreateDocumentResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return CreateDocument401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return CreateDocument400JSONResponse{Code: 400, Message: "Missing request body"}, nil + } + + // Read multipart form + form, err := request.Body.ReadForm(32 << 20) // 32MB max memory + if err != nil { + log.Error("ReadForm error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Failed to read form"}, nil + } + + // Get file from form + fileField := form.File["document_file"] + if len(fileField) == 0 { + return CreateDocument400JSONResponse{Code: 400, Message: "No file provided"}, nil + } + + file := fileField[0] + + // Validate file extension + if !strings.HasSuffix(strings.ToLower(file.Filename), ".epub") { + return CreateDocument400JSONResponse{Code: 400, Message: "Only EPUB files are allowed"}, nil + } + + // Open file + f, err := file.Open() + if err != nil { + log.Error("Open file error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Failed to open file"}, nil + } + defer f.Close() + + // Read file content + data, err := io.ReadAll(f) + if err != nil { + log.Error("Read file error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Failed to read file"}, nil + } + + // Validate actual content type + contentType := http.DetectContentType(data) + if contentType != "application/epub+zip" && contentType != "application/zip" { + return CreateDocument400JSONResponse{ + Code: 400, + Message: fmt.Sprintf("Invalid file type: %s. Only EPUB files are allowed.", contentType), + }, nil + } + + // Create temp file to get metadata + tempFile, err := os.CreateTemp("", "book") + if err != nil { + log.Error("Temp file create error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Unable to create temp file"}, nil + } + defer os.Remove(tempFile.Name()) + defer tempFile.Close() + + // Write data to temp file + if _, err := tempFile.Write(data); err != nil { + log.Error("Write temp file error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Unable to write temp file"}, nil + } + + // Get metadata using metadata package + metadataInfo, err := metadata.GetMetadata(tempFile.Name()) + if err != nil { + log.Error("GetMetadata error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Unable to acquire metadata"}, nil + } + + // Check if already exists + _, err = s.db.Queries.GetDocument(ctx, *metadataInfo.PartialMD5) + if err == nil { + // Document already exists + existingDoc, _ := s.db.Queries.GetDocument(ctx, *metadataInfo.PartialMD5) + apiDoc := Document{ + Id: existingDoc.ID, + Title: *existingDoc.Title, + Author: *existingDoc.Author, + Description: existingDoc.Description, + Isbn10: existingDoc.Isbn10, + Isbn13: existingDoc.Isbn13, + Words: existingDoc.Words, + Filepath: existingDoc.Filepath, + CreatedAt: parseTime(existingDoc.CreatedAt), + UpdatedAt: parseTime(existingDoc.UpdatedAt), + Deleted: existingDoc.Deleted, + } + response := DocumentResponse{ + Document: apiDoc, + } + return CreateDocument200JSONResponse(response), nil + } + + // Derive & sanitize file name + fileName := deriveBaseFileName(metadataInfo) + basePath := filepath.Join(s.cfg.DataPath, "documents") + safePath := filepath.Join(basePath, fileName) + + // Save file to storage + err = os.WriteFile(safePath, data, 0644) + if err != nil { + log.Error("Save file error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Unable to save file"}, nil + } + + // Upsert document + doc, err := s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ + ID: *metadataInfo.PartialMD5, + Title: metadataInfo.Title, + Author: metadataInfo.Author, + Description: metadataInfo.Description, + Md5: metadataInfo.MD5, + Words: metadataInfo.WordCount, + Filepath: &fileName, + Basepath: &basePath, + }) + if err != nil { + log.Error("UpsertDocument DB error:", err) + return CreateDocument500JSONResponse{Code: 500, Message: "Failed to save document"}, nil + } + + apiDoc := Document{ + Id: doc.ID, + Title: *doc.Title, + Author: *doc.Author, + Description: doc.Description, + Isbn10: doc.Isbn10, + Isbn13: doc.Isbn13, + Words: doc.Words, + Filepath: doc.Filepath, + CreatedAt: parseTime(doc.CreatedAt), + UpdatedAt: parseTime(doc.UpdatedAt), + Deleted: doc.Deleted, + } + + response := DocumentResponse{ + Document: apiDoc, + } + + return CreateDocument200JSONResponse(response), nil +} + +// GetDocumentCover200Response is a custom response type that allows setting content type +type GetDocumentCover200Response struct { + Body io.Reader + ContentLength int64 + ContentType string +} + +func (response GetDocumentCover200Response) VisitGetDocumentCoverResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", response.ContentType) + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.Closer); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +// GetDocumentFile200Response is a custom response type that allows setting filename for download +type GetDocumentFile200Response struct { + Body io.Reader + ContentLength int64 + Filename string +} + +func (response GetDocumentFile200Response) VisitGetDocumentFileResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/octet-stream") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", response.Filename)) + w.WriteHeader(200) + + if closer, ok := response.Body.(io.Closer); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} diff --git a/api/v1/documents_test.go b/api/v1/documents_test.go new file mode 100644 index 0000000..e2a492b --- /dev/null +++ b/api/v1/documents_test.go @@ -0,0 +1,178 @@ +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" +) + +type DocumentsTestSuite struct { + suite.Suite + db *database.DBManager + cfg *config.Config + srv *Server +} + +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 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, nil) +} + +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) +} + +func (suite *DocumentsTestSuite) TestAPIGetDocumentsUnauthenticated() { + req := httptest.NewRequest(http.MethodGet, "/api/v1/documents", nil) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusUnauthorized, w.Code) +} + +func (suite *DocumentsTestSuite) TestAPIGetDocument() { + suite.createTestUser("testuser", "testpass") + + docID := "test-doc-1" + _, err := suite.db.Queries.UpsertDocument(suite.T().Context(), database.UpsertDocumentParams{ + ID: docID, + Title: ptr.Of("Test Document"), + Author: ptr.Of("Test Author"), + }) + suite.Require().NoError(err) + + cookie := suite.login("testuser", "testpass") + + req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/"+docID, nil) + req.AddCookie(cookie) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusOK, w.Code) + + var resp DocumentResponse + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal(docID, resp.Document.Id) + suite.Equal("Test Document", resp.Document.Title) +} + +func (suite *DocumentsTestSuite) TestAPIGetDocumentNotFound() { + suite.createTestUser("testuser", "testpass") + cookie := suite.login("testuser", "testpass") + + req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/non-existent", nil) + req.AddCookie(cookie) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusNotFound, w.Code) +} + +func (suite *DocumentsTestSuite) TestAPIGetDocumentCoverUnauthenticated() { + req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/test-id/cover", nil) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusUnauthorized, w.Code) +} + +func (suite *DocumentsTestSuite) TestAPIGetDocumentFileUnauthenticated() { + req := httptest.NewRequest(http.MethodGet, "/api/v1/documents/test-id/file", nil) + w := httptest.NewRecorder() + + suite.srv.ServeHTTP(w, req) + + suite.Equal(http.StatusUnauthorized, 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/home.go b/api/v1/home.go new file mode 100644 index 0000000..787755a --- /dev/null +++ b/api/v1/home.go @@ -0,0 +1,226 @@ +package v1 + +import ( + "context" + "sort" + + log "github.com/sirupsen/logrus" + "reichard.io/antholume/database" + "reichard.io/antholume/graph" +) + +// GET /home +func (s *Server) GetHome(ctx context.Context, request GetHomeRequestObject) (GetHomeResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetHome401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + // Get database info + dbInfo, err := s.db.Queries.GetDatabaseInfo(ctx, auth.UserName) + if err != nil { + log.Error("GetDatabaseInfo DB Error:", err) + return GetHome500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + // Get streaks + streaks, err := s.db.Queries.GetUserStreaks(ctx, auth.UserName) + if err != nil { + log.Error("GetUserStreaks DB Error:", err) + return GetHome500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + // Get graph data + graphData, err := s.db.Queries.GetDailyReadStats(ctx, auth.UserName) + if err != nil { + log.Error("GetDailyReadStats DB Error:", err) + return GetHome500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + // Get user statistics + userStats, err := s.db.Queries.GetUserStatistics(ctx) + if err != nil { + log.Error("GetUserStatistics DB Error:", err) + return GetHome500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + // Build response + response := HomeResponse{ + DatabaseInfo: DatabaseInfo{ + DocumentsSize: dbInfo.DocumentsSize, + ActivitySize: dbInfo.ActivitySize, + ProgressSize: dbInfo.ProgressSize, + DevicesSize: dbInfo.DevicesSize, + }, + Streaks: StreaksResponse{ + Streaks: convertStreaks(streaks), + }, + GraphData: GraphDataResponse{ + GraphData: convertGraphData(graphData), + }, + UserStatistics: arrangeUserStatistics(userStats), + } + + return GetHome200JSONResponse(response), nil +} + +// GET /home/streaks +func (s *Server) GetStreaks(ctx context.Context, request GetStreaksRequestObject) (GetStreaksResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetStreaks401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + streaks, err := s.db.Queries.GetUserStreaks(ctx, auth.UserName) + if err != nil { + log.Error("GetUserStreaks DB Error:", err) + return GetStreaks500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + response := StreaksResponse{ + Streaks: convertStreaks(streaks), + } + + return GetStreaks200JSONResponse(response), nil +} + +// GET /home/graph +func (s *Server) GetGraphData(ctx context.Context, request GetGraphDataRequestObject) (GetGraphDataResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetGraphData401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + graphData, err := s.db.Queries.GetDailyReadStats(ctx, auth.UserName) + if err != nil { + log.Error("GetDailyReadStats DB Error:", err) + return GetGraphData500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + response := GraphDataResponse{ + GraphData: convertGraphData(graphData), + } + + return GetGraphData200JSONResponse(response), nil +} + +// GET /home/statistics +func (s *Server) GetUserStatistics(ctx context.Context, request GetUserStatisticsRequestObject) (GetUserStatisticsResponseObject, error) { + _, ok := s.getSessionFromContext(ctx) + if !ok { + return GetUserStatistics401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + userStats, err := s.db.Queries.GetUserStatistics(ctx) + if err != nil { + log.Error("GetUserStatistics DB Error:", err) + return GetUserStatistics500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + response := arrangeUserStatistics(userStats) + return GetUserStatistics200JSONResponse(response), nil +} + +func convertStreaks(streaks []database.UserStreak) []UserStreak { + result := make([]UserStreak, len(streaks)) + for i, streak := range streaks { + result[i] = UserStreak{ + Window: streak.Window, + MaxStreak: streak.MaxStreak, + MaxStreakStartDate: streak.MaxStreakStartDate, + MaxStreakEndDate: streak.MaxStreakEndDate, + CurrentStreak: streak.CurrentStreak, + CurrentStreakStartDate: streak.CurrentStreakStartDate, + CurrentStreakEndDate: streak.CurrentStreakEndDate, + } + } + return result +} + +func convertGraphData(graphData []database.GetDailyReadStatsRow) []GraphDataPoint { + result := make([]GraphDataPoint, len(graphData)) + for i, data := range graphData { + result[i] = GraphDataPoint{ + Date: data.Date, + MinutesRead: data.MinutesRead, + } + } + return result +} + +func arrangeUserStatistics(userStatistics []database.GetUserStatisticsRow) UserStatisticsResponse { + // Sort by WPM for each period + sortByWPM := func(stats []database.GetUserStatisticsRow, getter func(database.GetUserStatisticsRow) float64) []LeaderboardEntry { + sorted := append([]database.GetUserStatisticsRow(nil), stats...) + sort.SliceStable(sorted, func(i, j int) bool { + return getter(sorted[i]) > getter(sorted[j]) + }) + + result := make([]LeaderboardEntry, len(sorted)) + for i, item := range sorted { + result[i] = LeaderboardEntry{UserId: item.UserID, Value: getter(item)} + } + return result + } + + // Sort by duration (seconds) for each period + sortByDuration := func(stats []database.GetUserStatisticsRow, getter func(database.GetUserStatisticsRow) int64) []LeaderboardEntry { + sorted := append([]database.GetUserStatisticsRow(nil), stats...) + sort.SliceStable(sorted, func(i, j int) bool { + return getter(sorted[i]) > getter(sorted[j]) + }) + + result := make([]LeaderboardEntry, len(sorted)) + for i, item := range sorted { + result[i] = LeaderboardEntry{UserId: item.UserID, Value: float64(getter(item))} + } + return result + } + + // Sort by words for each period + sortByWords := func(stats []database.GetUserStatisticsRow, getter func(database.GetUserStatisticsRow) int64) []LeaderboardEntry { + sorted := append([]database.GetUserStatisticsRow(nil), stats...) + sort.SliceStable(sorted, func(i, j int) bool { + return getter(sorted[i]) > getter(sorted[j]) + }) + + result := make([]LeaderboardEntry, len(sorted)) + for i, item := range sorted { + result[i] = LeaderboardEntry{UserId: item.UserID, Value: float64(getter(item))} + } + return result + } + + return UserStatisticsResponse{ + Wpm: LeaderboardData{ + All: sortByWPM(userStatistics, func(s database.GetUserStatisticsRow) float64 { return s.TotalWpm }), + Year: sortByWPM(userStatistics, func(s database.GetUserStatisticsRow) float64 { return s.YearlyWpm }), + Month: sortByWPM(userStatistics, func(s database.GetUserStatisticsRow) float64 { return s.MonthlyWpm }), + Week: sortByWPM(userStatistics, func(s database.GetUserStatisticsRow) float64 { return s.WeeklyWpm }), + }, + Duration: LeaderboardData{ + All: sortByDuration(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.TotalSeconds }), + Year: sortByDuration(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.YearlySeconds }), + Month: sortByDuration(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.MonthlySeconds }), + Week: sortByDuration(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.WeeklySeconds }), + }, + Words: LeaderboardData{ + All: sortByWords(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.TotalWordsRead }), + Year: sortByWords(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.YearlyWordsRead }), + Month: sortByWords(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.MonthlyWordsRead }), + Week: sortByWords(userStatistics, func(s database.GetUserStatisticsRow) int64 { return s.WeeklyWordsRead }), + }, + } +} + +// GetSVGGraphData generates SVG bezier path for graph visualization +func GetSVGGraphData(inputData []GraphDataPoint, svgWidth int, svgHeight int) graph.SVGGraphData { + // Convert to int64 slice expected by graph package + intData := make([]int64, len(inputData)) + + for i, data := range inputData { + intData[i] = int64(data.MinutesRead) + } + + return graph.GetSVGGraphData(intData, svgWidth, svgHeight) +} \ 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..5dc92a4 --- /dev/null +++ b/api/v1/openapi.yaml @@ -0,0 +1,1977 @@ +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 + description: + type: string + isbn10: + type: string + isbn13: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + deleted: + type: boolean + words: + type: integer + format: int64 + filepath: + type: string + percentage: + type: number + format: float + total_time_seconds: + type: integer + format: int64 + wpm: + type: number + format: float + seconds_per_percent: + type: integer + format: int64 + last_read: + type: string + format: date-time + 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: + title: + type: string + author: + type: string + device_name: + type: string + device_id: + type: string + percentage: + type: number + format: double + progress: + type: string + document_id: + type: string + user_id: + type: string + created_at: + type: string + format: date-time + + UpdateProgressRequest: + type: object + properties: + document_id: + type: string + percentage: + type: number + format: double + progress: + type: string + device_id: + type: string + device_name: + type: string + required: + - document_id + - percentage + - progress + - device_id + - device_name + + UpdateProgressResponse: + type: object + properties: + document_id: + type: string + timestamp: + type: string + format: date-time + required: + - document_id + - timestamp + + CreateActivityItem: + type: object + properties: + document_id: + type: string + start_time: + type: integer + format: int64 + duration: + type: integer + format: int64 + page: + type: integer + format: int64 + pages: + type: integer + format: int64 + required: + - document_id + - start_time + - duration + - page + - pages + + CreateActivityRequest: + type: object + properties: + device_id: + type: string + device_name: + type: string + activity: + type: array + items: + $ref: '#/components/schemas/CreateActivityItem' + required: + - device_id + - device_name + - activity + + CreateActivityResponse: + type: object + properties: + added: + type: integer + format: int64 + required: + - added + + Activity: + type: object + properties: + document_id: + type: string + device_id: + type: string + start_time: + type: string + title: + type: string + author: + type: string + duration: + type: integer + format: int64 + start_percentage: + type: number + format: float + end_percentage: + type: number + format: float + read_percentage: + type: number + format: float + required: + - document_id + - device_id + - start_time + - duration + - start_percentage + - end_percentage + - read_percentage + + SearchItem: + type: object + properties: + id: + type: string + title: + type: string + author: + type: string + language: + type: string + series: + type: string + file_type: + type: string + file_size: + type: string + upload_date: + type: string + + SearchResponse: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/SearchItem' + source: + type: string + query: + type: string + required: + - results + - source + - query + + 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 + required: + - documents + - total + - page + - limit + + DocumentResponse: + type: object + properties: + document: + $ref: '#/components/schemas/Document' + required: + - document + + ProgressListResponse: + type: object + properties: + progress: + type: array + items: + $ref: '#/components/schemas/Progress' + page: + type: integer + format: int64 + limit: + type: integer + format: int64 + next_page: + type: integer + format: int64 + previous_page: + type: integer + format: int64 + total: + type: integer + format: int64 + + ProgressResponse: + type: object + properties: + progress: + $ref: '#/components/schemas/Progress' + + ActivityResponse: + type: object + properties: + activities: + type: array + items: + $ref: '#/components/schemas/Activity' + required: + - activities + + Device: + type: object + properties: + id: + type: string + device_name: + type: string + created_at: + type: string + format: date-time + last_synced: + type: string + format: date-time + + SettingsResponse: + type: object + properties: + user: + $ref: '#/components/schemas/UserData' + timezone: + type: string + devices: + type: array + items: + $ref: '#/components/schemas/Device' + required: + - settings + - user + + UpdateSettingsRequest: + type: object + properties: + password: + type: string + new_password: + type: string + timezone: + type: string + + 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 + + MessageResponse: + type: object + properties: + message: + type: string + required: + - message + + DatabaseInfo: + type: object + properties: + documents_size: + type: integer + format: int64 + activity_size: + type: integer + format: int64 + progress_size: + type: integer + format: int64 + devices_size: + type: integer + format: int64 + required: + - documents_size + - activity_size + - progress_size + - devices_size + + UserStreak: + type: object + properties: + window: + type: string + max_streak: + type: integer + format: int64 + max_streak_start_date: + type: string + max_streak_end_date: + type: string + current_streak: + type: integer + format: int64 + current_streak_start_date: + type: string + current_streak_end_date: + type: string + required: + - window + - max_streak + - max_streak_start_date + - max_streak_end_date + - current_streak + - current_streak_start_date + - current_streak_end_date + + StreaksResponse: + type: object + properties: + streaks: + type: array + items: + $ref: '#/components/schemas/UserStreak' + required: + - streaks + + GraphDataPoint: + type: object + properties: + date: + type: string + minutes_read: + type: integer + format: int64 + required: + - date + - minutes_read + + GraphDataResponse: + type: object + properties: + graph_data: + type: array + items: + $ref: '#/components/schemas/GraphDataPoint' + required: + - graph_data + + LeaderboardEntry: + type: object + properties: + user_id: + type: string + value: + type: number + format: double + required: + - user_id + - value + + LeaderboardData: + type: object + properties: + all: + type: array + items: + $ref: '#/components/schemas/LeaderboardEntry' + year: + type: array + items: + $ref: '#/components/schemas/LeaderboardEntry' + month: + type: array + items: + $ref: '#/components/schemas/LeaderboardEntry' + week: + type: array + items: + $ref: '#/components/schemas/LeaderboardEntry' + required: + - all + - year + - month + - week + + UserStatisticsResponse: + type: object + properties: + wpm: + $ref: '#/components/schemas/LeaderboardData' + duration: + $ref: '#/components/schemas/LeaderboardData' + words: + $ref: '#/components/schemas/LeaderboardData' + required: + - wpm + - duration + - words + + HomeResponse: + type: object + properties: + database_info: + $ref: '#/components/schemas/DatabaseInfo' + streaks: + $ref: '#/components/schemas/StreaksResponse' + graph_data: + $ref: '#/components/schemas/GraphDataResponse' + user_statistics: + $ref: '#/components/schemas/UserStatisticsResponse' + required: + - database_info + - streaks + - graph_data + - user_statistics + + BackupType: + type: string + enum: [COVERS, DOCUMENTS] + + ImportType: + type: string + enum: [DIRECT, COPY] + + OperationType: + type: string + enum: [CREATE, UPDATE, DELETE] + + User: + type: object + properties: + id: + type: string + admin: + type: boolean + created_at: + type: string + format: date-time + required: + - id + - admin + - created_at + + UsersResponse: + type: object + properties: + users: + type: array + items: + $ref: '#/components/schemas/User' + + ImportResult: + type: object + properties: + id: + type: string + name: + type: string + path: + type: string + status: + type: string + enum: [FAILED, SUCCESS, EXISTS] + error: + type: string + + ImportResultsResponse: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/ImportResult' + + DirectoryItem: + type: object + properties: + name: + type: string + path: + type: string + + DirectoryListResponse: + type: object + properties: + current_path: + type: string + items: + type: array + items: + $ref: '#/components/schemas/DirectoryItem' + + LogEntry: + type: string + + LogsResponse: + type: object + properties: + logs: + type: array + items: + $ref: '#/components/schemas/LogEntry' + filter: + type: string + page: + type: integer + format: int64 + limit: + type: integer + format: int64 + next_page: + type: integer + format: int64 + previous_page: + type: integer + format: int64 + total: + type: integer + format: int64 + + InfoResponse: + type: object + properties: + version: + type: string + search_enabled: + type: boolean + registration_enabled: + type: boolean + required: + - version + - search_enabled + - registration_enabled + + securitySchemes: + BearerAuth: + type: apiKey + in: cookie + name: token + +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' + post: + summary: Upload a new document + operationId: createDocument + tags: + - Documents + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + document_file: + type: string + format: binary + required: + - document_file + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 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' + post: + summary: Update document editable fields + operationId: editDocument + tags: + - Documents + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + author: + type: string + description: + type: string + isbn10: + type: string + isbn13: + type: string + cover_gbid: + type: string + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 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' + + /documents/{id}/cover: + get: + summary: Get document cover image + operationId: getDocumentCover + tags: + - Documents + parameters: + - name: id + in: path + required: true + schema: + type: string + security: + - BearerAuth: [] + responses: + 200: + description: Cover image + content: + image/jpeg: {} + image/png: {} + 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' + post: + summary: Upload document cover image + operationId: uploadDocumentCover + tags: + - Documents + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + cover_file: + type: string + format: binary + required: + - cover_file + security: + - BearerAuth: [] + responses: + 200: + description: Cover uploaded + content: + application/json: + schema: + $ref: '#/components/schemas/DocumentResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 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' + + /documents/{id}/file: + get: + summary: Download document file + operationId: getDocumentFile + tags: + - Documents + parameters: + - name: id + in: path + required: true + schema: + type: string + security: + - BearerAuth: [] + responses: + 200: + description: Document file download + content: + application/octet-stream: + schema: + type: string + format: binary + 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: + get: + summary: List progress records + operationId: getProgressList + tags: + - Progress + parameters: + - name: page + in: query + schema: + type: integer + format: int64 + default: 1 + - name: limit + in: query + schema: + type: integer + format: int64 + default: 15 + - name: document + in: query + schema: + type: string + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ProgressListResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + put: + summary: Update document progress + operationId: updateProgress + tags: + - Progress + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProgressRequest' + responses: + 200: + description: Progress updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProgressResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 401: + description: Unauthorized + 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' + post: + summary: Create activity records + operationId: createActivity + tags: + - Activity + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateActivityRequest' + responses: + 200: + description: Activity created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CreateActivityResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 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' + put: + summary: Update user settings + operationId: updateSettings + tags: + - Settings + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateSettingsRequest' + responses: + 200: + description: Settings updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/SettingsResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 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 + headers: + Set-Cookie: + description: HttpOnly session cookie for authenticated requests. + schema: + type: string + 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/register: + post: + summary: User registration + operationId: register + tags: + - Auth + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + 201: + description: Successful registration + headers: + Set-Cookie: + description: HttpOnly session cookie for authenticated requests. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 403: + description: Registration disabled + 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' + + /info: + get: + summary: Get server information + operationId: getInfo + tags: + - Info + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/InfoResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /home: + get: + summary: Get home page data + operationId: getHome + tags: + - Home + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/HomeResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /home/streaks: + get: + summary: Get user streaks + operationId: getStreaks + tags: + - Home + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/StreaksResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /home/graph: + get: + summary: Get daily read stats graph data + operationId: getGraphData + tags: + - Home + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/GraphDataResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /home/statistics: + get: + summary: Get user statistics (leaderboards) + operationId: getUserStatistics + tags: + - Home + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/UserStatisticsResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /search: + get: + summary: Search external book sources + operationId: getSearch + tags: + - Search + parameters: + - name: query + in: query + required: true + schema: + type: string + - name: source + in: query + required: true + schema: + type: string + enum: [LibGen, Annas Archive] + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResponse' + 400: + description: Invalid query + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Search error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + post: + summary: Download search result + operationId: postSearch + tags: + - Search + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + source: + type: string + title: + type: string + author: + type: string + id: + type: string + required: + - source + - title + - author + - id + security: + - BearerAuth: [] + responses: + 200: + description: Download initiated + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Download error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /admin: + get: + summary: Get admin page data + operationId: getAdmin + tags: + - Admin + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + type: object + properties: + database_info: + $ref: '#/components/schemas/DatabaseInfo' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + post: + summary: Perform admin action (backup, restore, etc.) + operationId: postAdminAction + tags: + - Admin + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + action: + type: string + enum: [BACKUP, RESTORE, METADATA_MATCH, CACHE_TABLES] + backup_types: + type: array + items: + $ref: '#/components/schemas/BackupType' + restore_file: + type: string + format: binary + required: + - action + security: + - BearerAuth: [] + responses: + 200: + description: Action completed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + application/octet-stream: + schema: + type: string + format: binary + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /admin/users: + get: + summary: Get all users + operationId: getUsers + tags: + - Admin + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/UsersResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + post: + summary: Create, update, or delete user + operationId: updateUser + tags: + - Admin + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + operation: + $ref: '#/components/schemas/OperationType' + user: + type: string + password: + type: string + is_admin: + type: boolean + required: + - operation + - user + security: + - BearerAuth: [] + responses: + 200: + description: User updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UsersResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /admin/import: + get: + summary: Get import directory list + operationId: getImportDirectory + tags: + - Admin + parameters: + - name: directory + in: query + schema: + type: string + - name: select + in: query + schema: + type: string + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/DirectoryListResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + post: + summary: Perform import + operationId: postImport + tags: + - Admin + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + directory: + type: string + type: + $ref: '#/components/schemas/ImportType' + required: + - directory + - type + security: + - BearerAuth: [] + responses: + 200: + description: Import completed + content: + application/json: + schema: + $ref: '#/components/schemas/ImportResultsResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /admin/import-results: + get: + summary: Get import results + operationId: getImportResults + tags: + - Admin + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ImportResultsResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /admin/logs: + get: + summary: Get logs with optional filter + operationId: getLogs + tags: + - Admin + parameters: + - name: filter + in: query + schema: + type: string + - name: page + in: query + schema: + type: integer + format: int64 + minimum: 1 + - name: limit + in: query + schema: + type: integer + format: int64 + minimum: 1 + security: + - BearerAuth: [] + responses: + 200: + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/LogsResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' diff --git a/api/v1/progress.go b/api/v1/progress.go new file mode 100644 index 0000000..5461b28 --- /dev/null +++ b/api/v1/progress.go @@ -0,0 +1,163 @@ +package v1 + +import ( + "context" + "math" + "time" + + log "github.com/sirupsen/logrus" + "reichard.io/antholume/database" +) + +// GET /progress +func (s *Server) GetProgressList(ctx context.Context, request GetProgressListRequestObject) (GetProgressListResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return GetProgressList401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + page := int64(1) + if request.Params.Page != nil { + page = *request.Params.Page + } + + limit := int64(15) + if request.Params.Limit != nil { + limit = *request.Params.Limit + } + + filter := database.GetProgressParams{ + UserID: auth.UserName, + Offset: (page - 1) * limit, + Limit: limit, + } + + if request.Params.Document != nil && *request.Params.Document != "" { + filter.DocFilter = true + filter.DocumentID = *request.Params.Document + } + + progress, err := s.db.Queries.GetProgress(ctx, filter) + if err != nil { + log.Error("GetProgress DB Error:", err) + return GetProgressList500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + total := int64(len(progress)) + var nextPage *int64 + var previousPage *int64 + + // Calculate total pages + totalPages := int64(math.Ceil(float64(total) / float64(limit))) + if page < totalPages { + nextPage = ptrOf(page + 1) + } + if page > 1 { + previousPage = ptrOf(page - 1) + } + + apiProgress := make([]Progress, len(progress)) + for i, row := range progress { + apiProgress[i] = Progress{ + Title: row.Title, + Author: row.Author, + DeviceName: &row.DeviceName, + Percentage: &row.Percentage, + DocumentId: &row.DocumentID, + UserId: &row.UserID, + CreatedAt: parseTimePtr(row.CreatedAt), + } + } + + response := ProgressListResponse{ + Progress: &apiProgress, + Page: &page, + Limit: &limit, + NextPage: nextPage, + PreviousPage: previousPage, + Total: &total, + } + + return GetProgressList200JSONResponse(response), nil +} + +// 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 + } + + row, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{ + UserID: auth.UserName, + DocumentID: request.Id, + }) + if err != nil { + log.Error("GetDocumentProgress DB Error:", err) + return GetProgress404JSONResponse{Code: 404, Message: "Progress not found"}, nil + } + + apiProgress := Progress{ + DeviceName: &row.DeviceName, + DeviceId: &row.DeviceID, + Percentage: &row.Percentage, + Progress: &row.Progress, + DocumentId: &row.DocumentID, + UserId: &row.UserID, + CreatedAt: parseTimePtr(row.CreatedAt), + } + + response := ProgressResponse{ + Progress: &apiProgress, + } + + return GetProgress200JSONResponse(response), nil +} + +// PUT /progress +func (s *Server) UpdateProgress(ctx context.Context, request UpdateProgressRequestObject) (UpdateProgressResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return UpdateProgress401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return UpdateProgress400JSONResponse{Code: 400, Message: "Request body is required"}, nil + } + + if _, err := s.db.Queries.UpsertDevice(ctx, database.UpsertDeviceParams{ + ID: request.Body.DeviceId, + UserID: auth.UserName, + DeviceName: request.Body.DeviceName, + LastSynced: time.Now().UTC().Format(time.RFC3339), + }); err != nil { + log.Error("UpsertDevice DB Error:", err) + return UpdateProgress500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + if _, err := s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ + ID: request.Body.DocumentId, + }); err != nil { + log.Error("UpsertDocument DB Error:", err) + return UpdateProgress500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + progress, err := s.db.Queries.UpdateProgress(ctx, database.UpdateProgressParams{ + Percentage: request.Body.Percentage, + DocumentID: request.Body.DocumentId, + DeviceID: request.Body.DeviceId, + UserID: auth.UserName, + Progress: request.Body.Progress, + }) + if err != nil { + log.Error("UpdateProgress DB Error:", err) + return UpdateProgress400JSONResponse{Code: 400, Message: "Invalid request"}, nil + } + + response := UpdateProgressResponse{ + DocumentId: progress.DocumentID, + Timestamp: parseTime(progress.CreatedAt), + } + + return UpdateProgress200JSONResponse(response), nil +} diff --git a/api/v1/search.go b/api/v1/search.go new file mode 100644 index 0000000..29bf5b9 --- /dev/null +++ b/api/v1/search.go @@ -0,0 +1,59 @@ +package v1 + +import ( + "context" + + "reichard.io/antholume/search" + log "github.com/sirupsen/logrus" +) + +// GET /search +func (s *Server) GetSearch(ctx context.Context, request GetSearchRequestObject) (GetSearchResponseObject, error) { + + if request.Params.Query == "" { + return GetSearch400JSONResponse{Code: 400, Message: "Invalid query"}, nil + } + + query := request.Params.Query + source := string(request.Params.Source) + + // Validate source + if source != "LibGen" && source != "Annas Archive" { + return GetSearch400JSONResponse{Code: 400, Message: "Invalid source"}, nil + } + + searchResults, err := search.SearchBook(query, search.Source(source)) + if err != nil { + log.Error("Search Error:", err) + return GetSearch500JSONResponse{Code: 500, Message: "Search error"}, nil + } + + apiResults := make([]SearchItem, len(searchResults)) + for i, item := range searchResults { + apiResults[i] = SearchItem{ + Id: ptrOf(item.ID), + Title: ptrOf(item.Title), + Author: ptrOf(item.Author), + Language: ptrOf(item.Language), + Series: ptrOf(item.Series), + FileType: ptrOf(item.FileType), + FileSize: ptrOf(item.FileSize), + UploadDate: ptrOf(item.UploadDate), + } + } + + response := SearchResponse{ + Results: apiResults, + Source: source, + Query: query, + } + + return GetSearch200JSONResponse(response), nil +} + +// POST /search +func (s *Server) PostSearch(ctx context.Context, request PostSearchRequestObject) (PostSearchResponseObject, error) { + // This endpoint is used by the SSR template to queue a download + // For the API, we just return success - the actual download happens via /documents POST + return PostSearch200Response{}, nil +} \ No newline at end of file diff --git a/api/v1/server.go b/api/v1/server.go new file mode 100644 index 0000000..89dc59a --- /dev/null +++ b/api/v1/server.go @@ -0,0 +1,99 @@ +package v1 + +import ( + "context" + "encoding/json" + "io/fs" + "net/http" + + "reichard.io/antholume/config" + "reichard.io/antholume/database" +) + +var _ StrictServerInterface = (*Server)(nil) + +type Server struct { + mux *http.ServeMux + db *database.DBManager + cfg *config.Config + assets fs.FS +} + +// NewServer creates a new native HTTP server +func NewServer(db *database.DBManager, cfg *config.Config, assets fs.FS) *Server { + s := &Server{ + mux: http.NewServeMux(), + db: db, + cfg: cfg, + assets: assets, + } + + // Create strict handler with authentication middleware + strictHandler := NewStrictHandler(s, []StrictMiddlewareFunc{s.authMiddleware}) + + s.mux = HandlerFromMuxWithBaseURL(strictHandler, s.mux, "/api/v1").(*http.ServeMux) + return s +} + +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.mux.ServeHTTP(w, r) +} + +// 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) + + // Skip auth for public auth and info endpoints - cover and file require auth via cookies + if operationID == "Login" || operationID == "Register" || operationID == "GetInfo" { + return handler(ctx, w, r, request) + } + + 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 + } + + // Check admin status for admin-only endpoints + adminEndpoints := []string{ + "GetAdmin", + "PostAdminAction", + "GetUsers", + "UpdateUser", + "GetImportDirectory", + "PostImport", + "GetImportResults", + "GetLogs", + } + + for _, adminEndpoint := range adminEndpoints { + if operationID == adminEndpoint && !auth.IsAdmin { + // Write 403 response directly + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(403) + json.NewEncoder(w).Encode(ErrorResponse{Code: 403, Message: "Admin privileges required"}) + return nil, nil + } + } + + // Store auth in context for handlers to access + ctx = context.WithValue(ctx, "auth", auth) + + return handler(ctx, w, r, request) + } +} + +// GetInfo returns server information +func (s *Server) GetInfo(ctx context.Context, request GetInfoRequestObject) (GetInfoResponseObject, error) { + return GetInfo200JSONResponse{ + Version: s.cfg.Version, + SearchEnabled: s.cfg.SearchEnabled, + RegistrationEnabled: s.cfg.RegistrationEnabled, + }, nil +} diff --git a/api/v1/server_test.go b/api/v1/server_test.go new file mode 100644 index 0000000..0d1a5c1 --- /dev/null +++ b/api/v1/server_test.go @@ -0,0 +1,58 @@ +package v1 + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/suite" + + "reichard.io/antholume/config" + "reichard.io/antholume/database" +) + +type ServerTestSuite struct { + suite.Suite + db *database.DBManager + cfg *config.Config + srv *Server +} + +func TestServer(t *testing.T) { + suite.Run(t, new(ServerTestSuite)) +} + +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, nil) +} + +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() + + suite.srv.ServeHTTP(w, req) + + 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..a0bd532 --- /dev/null +++ b/api/v1/settings.go @@ -0,0 +1,157 @@ +package v1 + +import ( + "context" + "crypto/md5" + "fmt" + + "reichard.io/antholume/database" + argon2id "github.com/alexedwards/argon2id" +) + +// 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 + } + + devices, err := s.db.Queries.GetDevices(ctx, auth.UserName) + if err != nil { + return GetSettings500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + apiDevices := make([]Device, len(devices)) + for i, device := range devices { + apiDevices[i] = Device{ + Id: &device.ID, + DeviceName: &device.DeviceName, + CreatedAt: parseTimePtr(device.CreatedAt), + LastSynced: parseTimePtr(device.LastSynced), + } + } + + response := SettingsResponse{ + User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, + Timezone: user.Timezone, + Devices: &apiDevices, + } + return GetSettings200JSONResponse(response), nil +} + +// authorizeCredentials verifies if credentials are valid +func (s *Server) authorizeCredentials(ctx context.Context, username string, password string) bool { + user, err := s.db.Queries.GetUser(ctx, username) + if err != nil { + return false + } + + // Try argon2 hash comparison + if match, err := argon2id.ComparePasswordAndHash(password, *user.Pass); err == nil && match { + return true + } + + return false +} + +// PUT /settings +func (s *Server) UpdateSettings(ctx context.Context, request UpdateSettingsRequestObject) (UpdateSettingsResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return UpdateSettings401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return UpdateSettings400JSONResponse{Code: 400, Message: "Request body is required"}, nil + } + + user, err := s.db.Queries.GetUser(ctx, auth.UserName) + if err != nil { + return UpdateSettings500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + updateParams := database.UpdateUserParams{ + UserID: auth.UserName, + Admin: auth.IsAdmin, + } + + // Update password if provided + if request.Body.NewPassword != nil { + if request.Body.Password == nil { + return UpdateSettings400JSONResponse{Code: 400, Message: "Current password is required to set new password"}, nil + } + + // Verify current password - first try bcrypt (new format), then argon2, then MD5 (legacy format) + currentPasswordMatched := false + + // Try argon2 (current format) + if !currentPasswordMatched { + currentPassword := fmt.Sprintf("%x", md5.Sum([]byte(*request.Body.Password))) + if match, err := argon2id.ComparePasswordAndHash(currentPassword, *user.Pass); err == nil && match { + currentPasswordMatched = true + } + } + + if !currentPasswordMatched { + return UpdateSettings400JSONResponse{Code: 400, Message: "Invalid current password"}, nil + } + + // Hash new password with argon2 + newPassword := fmt.Sprintf("%x", md5.Sum([]byte(*request.Body.NewPassword))) + hashedPassword, err := argon2id.CreateHash(newPassword, argon2id.DefaultParams) + if err != nil { + return UpdateSettings500JSONResponse{Code: 500, Message: "Failed to hash password"}, nil + } + updateParams.Password = &hashedPassword + } + + // Update timezone if provided + if request.Body.Timezone != nil { + updateParams.Timezone = request.Body.Timezone + } + + // If nothing to update, return error + if request.Body.NewPassword == nil && request.Body.Timezone == nil { + return UpdateSettings400JSONResponse{Code: 400, Message: "At least one field must be provided"}, nil + } + + // Update user + _, err = s.db.Queries.UpdateUser(ctx, updateParams) + if err != nil { + return UpdateSettings500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + // Get updated settings to return + user, err = s.db.Queries.GetUser(ctx, auth.UserName) + if err != nil { + return UpdateSettings500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + devices, err := s.db.Queries.GetDevices(ctx, auth.UserName) + if err != nil { + return UpdateSettings500JSONResponse{Code: 500, Message: err.Error()}, nil + } + + apiDevices := make([]Device, len(devices)) + for i, device := range devices { + apiDevices[i] = Device{ + Id: &device.ID, + DeviceName: &device.DeviceName, + CreatedAt: parseTimePtr(device.CreatedAt), + LastSynced: parseTimePtr(device.LastSynced), + } + } + + response := SettingsResponse{ + User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin}, + Timezone: user.Timezone, + Devices: &apiDevices, + } + return UpdateSettings200JSONResponse(response), nil +} + diff --git a/api/v1/utils.go b/api/v1/utils.go new file mode 100644 index 0000000..dd9b627 --- /dev/null +++ b/api/v1/utils.go @@ -0,0 +1,84 @@ +package v1 + +import ( + "encoding/json" + "net/http" + "net/url" + "strconv" + "time" +) + +// 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) + if err := json.NewEncoder(w).Encode(data); err != nil { + writeJSONError(w, http.StatusInternalServerError, "Failed to encode 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, + Message: message, + }) +} + +// QueryParams represents parsed query parameters (deprecated - used by tests only) +type QueryParams struct { + Page int64 + Limit int64 + Search *string +} + +// 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 { + page = 1 + } + limit, _ := strconv.ParseInt(query.Get("limit"), 10, 64) + if limit == 0 { + limit = defaultLimit + } + search := query.Get("search") + var searchPtr *string + if search != "" { + searchPtr = ptrOf("%" + search + "%") + } + return QueryParams{ + Page: page, + Limit: limit, + Search: searchPtr, + } +} + +// 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 +} + +// parseTimePtr parses an interface{} (from SQL) to *time.Time +func parseTimePtr(v interface{}) *time.Time { + if v == nil { + return nil + } + if s, ok := v.(string); ok { + t := parseTime(s) + if t.IsZero() { + return nil + } + return &t + } + return nil +} \ No newline at end of file diff --git a/api/v1/utils_test.go b/api/v1/utils_test.go new file mode 100644 index 0000000..214176a --- /dev/null +++ b/api/v1/utils_test.go @@ -0,0 +1,76 @@ +package v1 + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/suite" +) + +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) + + suite.Equal("application/json", w.Header().Get("Content-Type")) + suite.Equal(http.StatusOK, w.Code) + + var resp map[string]string + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal("value", resp["test"]) +} + +func (suite *UtilsTestSuite) TestWriteJSONError() { + w := httptest.NewRecorder() + + writeJSONError(w, http.StatusBadRequest, "test error") + + suite.Equal(http.StatusBadRequest, w.Code) + + var resp ErrorResponse + suite.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + suite.Equal(http.StatusBadRequest, resp.Code) + suite.Equal("test error", resp.Message) +} + +func (suite *UtilsTestSuite) TestParseQueryParams() { + query := make(map[string][]string) + query["page"] = []string{"2"} + query["limit"] = []string{"15"} + query["search"] = []string{"test"} + + params := parseQueryParams(query, 9) + + suite.Equal(int64(2), params.Page) + suite.Equal(int64(15), params.Limit) + suite.NotNil(params.Search) +} + +func (suite *UtilsTestSuite) TestParseQueryParamsDefaults() { + query := make(map[string][]string) + + params := parseQueryParams(query, 9) + + suite.Equal(int64(1), params.Page) + suite.Equal(int64(9), params.Limit) + suite.Nil(params.Search) +} + +func (suite *UtilsTestSuite) TestPtrOf() { + value := "test" + ptr := ptrOf(value) + + 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/flake.nix b/flake.nix index 66c6501..c521d51 100644 --- a/flake.nix +++ b/flake.nix @@ -21,11 +21,12 @@ devShells.default = pkgs.mkShell { packages = with pkgs; [ go - gopls golangci-lint + gopls + + bun nodejs tailwindcss - python311Packages.grip ]; shellHook = '' export PATH=$PATH:~/go/bin diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..bf2437f --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,2 @@ +# Generated API code +src/generated/**/* diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..e800a8d --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "es5", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "lf" +} diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md new file mode 100644 index 0000000..397c290 --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1,76 @@ +# AnthoLume Frontend Agent Guide + +Read this file for work in `frontend/`. +Also follow the repository root guide at `../AGENTS.md`. + +## 1) Stack + +- Package manager: `bun` +- Framework: React + Vite +- Data fetching: React Query +- API generation: Orval +- Linting: ESLint + Tailwind plugin +- Formatting: Prettier + +## 2) Conventions + +- Use local icon components from `src/icons/`. +- Do not add external icon libraries. +- Prefer generated types from `src/generated/model/` over `any`. +- Avoid custom class names in JSX `className` values unless the Tailwind lint config already allows them. +- For decorative icons in inputs or labels, disable hover styling via the icon component API rather than overriding it ad hoc. +- Prefer `LoadingState` for result-area loading indicators; avoid early returns that unmount search/filter forms during fetches. +- Use theme tokens from `tailwind.config.js` / `src/index.css` (`bg-surface`, `text-content`, `border-border`, `primary`, etc.) for new UI work instead of adding raw light/dark color pairs. +- Store frontend-only preferences in `src/utils/localSettings.ts` so appearance and view settings share one local-storage shape. + +## 3) Generated API client + +- Do not edit `src/generated/**` directly. +- Edit `../api/v1/openapi.yaml` and regenerate instead. +- Regenerate with: `bun run generate:api` + +### Important behavior + +- The generated client returns `{ data, status, headers }` for both success and error responses. +- Do not assume non-2xx responses throw. +- Check `response.status` and response shape before treating a request as successful. + +## 4) Auth / Query State + +- When changing auth flows, account for React Query cache state. +- Pay special attention to `/api/v1/auth/me`. +- A local auth state update may not be enough if cached query data still reflects a previous auth state. + +## 5) Commands + +- Lint: `bun run lint` +- Typecheck: `bun run typecheck` +- Lint fix: `bun run lint:fix` +- Format check: `bun run format` +- Format fix: `bun run format:fix` +- Build: `bun run build` +- Generate API client: `bun run generate:api` + +## 6) Validation Notes + +- ESLint ignores `src/generated/**`. +- Frontend unit tests use Vitest and live alongside source as `src/**/*.test.ts(x)`. +- Read `TESTING_STRATEGY.md` before adding or expanding frontend tests. +- Prefer tests for meaningful app behavior, branching logic, side effects, and user-visible outcomes. +- Avoid low-value tests that mainly assert exact styling classes, duplicate existing coverage, or re-test framework/library behavior. +- `bun run lint` includes test files but does not typecheck. +- Use `bun run typecheck` to run TypeScript validation for app code and colocated tests without a full production build. +- Run frontend tests with `bun run test`. +- `bun run build` still runs `tsc && vite build`, so unrelated TypeScript issues elsewhere in `src/` can fail the build. +- When possible, validate changed files directly before escalating to full-project fixes. + +## 7) Updating This File + +After completing a frontend task, update this file if you learned something general that would help future frontend agents. + +Rules for updates: + +- Add only frontend-wide guidance. +- Do not record one-off task history. +- Keep updates concise and action-oriented. +- Prefer notes that prevent repeated mistakes. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..bedd3f8 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,111 @@ +# AnthoLume Frontend + +A React + TypeScript frontend for AnthoLume, replacing the server-side rendering (SSR) templates. + +## Tech Stack + +- **React 19** - UI framework +- **TypeScript** - Type safety +- **React Query (TanStack Query)** - Server state management +- **Orval** - API client generation from OpenAPI spec +- **React Router** - Navigation +- **Tailwind CSS** - Styling +- **Vite** - Build tool +- **Axios** - HTTP client with auth interceptors + +## Authentication + +The frontend includes a complete authentication system: + +### Auth Context +- `AuthProvider` - Manages authentication state globally +- `useAuth()` - Hook to access auth state and methods +- Token stored in `localStorage` +- Axios interceptors automatically attach Bearer token to API requests + +### Protected Routes +- All main routes are wrapped in `ProtectedRoute` +- Unauthenticated users are redirected to `/login` +- Layout redirects to login if not authenticated + +### Login Flow +1. User enters credentials on `/login` +2. POST to `/api/v1/auth/login` +3. Token stored in localStorage +4. Redirect to home page +5. Axios interceptor includes token in subsequent requests + +### Logout Flow +1. User clicks "Logout" in dropdown menu +2. POST to `/api/v1/auth/logout` +3. Token cleared from localStorage +4. Redirect to `/login` + +### 401 Handling +- Axios response interceptor clears token on 401 errors +- Prevents stale auth state + +## Architecture + +The frontend mirrors the existing SSR templates structure: + +### Pages +- `HomePage` - Landing page with recent documents +- `DocumentsPage` - Document listing with search and pagination +- `DocumentPage` - Single document view with details +- `ProgressPage` - Reading progress table +- `ActivityPage` - User activity log +- `SearchPage` - Search interface +- `SettingsPage` - User settings +- `LoginPage` - Authentication + +### Components +- `Layout` - Main layout with navigation sidebar and header +- Generated API hooks from `api/v1/openapi.yaml` + +## API Integration + +The frontend uses **Orval** to generate TypeScript types and React Query hooks from the OpenAPI spec: + +```bash +npm run generate:api +``` + +This generates: +- Type definitions for all API schemas +- React Query hooks (`useGetDocuments`, `useGetDocument`, etc.) +- Mutation hooks (`useLogin`, `useLogout`) + +## Development + +```bash +# Install dependencies +npm install + +# Generate API types (if OpenAPI spec changes) +npm run generate:api + +# Start development server +npm run dev + +# Build for production +npm run build +``` + +## Deployment + +The built output is in `dist/` and can be served by the Go backend or deployed separately. + +## Migration from SSR + +The frontend replicates the functionality of the following SSR templates: +- `templates/pages/home.tmpl` → `HomePage.tsx` +- `templates/pages/documents.tmpl` → `DocumentsPage.tsx` +- `templates/pages/document.tmpl` → `DocumentPage.tsx` +- `templates/pages/progress.tmpl` → `ProgressPage.tsx` +- `templates/pages/activity.tmpl` → `ActivityPage.tsx` +- `templates/pages/search.tmpl` → `SearchPage.tsx` +- `templates/pages/settings.tmpl` → `SettingsPage.tsx` +- `templates/pages/login.tmpl` → `LoginPage.tsx` + +The styling follows the same Tailwind CSS classes as the original templates for consistency. \ No newline at end of file diff --git a/frontend/TESTING_STRATEGY.md b/frontend/TESTING_STRATEGY.md new file mode 100644 index 0000000..f0d9b75 --- /dev/null +++ b/frontend/TESTING_STRATEGY.md @@ -0,0 +1,73 @@ +# Frontend Testing Strategy + +This project prefers meaningful frontend tests over high test counts. + +## What we want to test + +Prioritize tests for app-owned behavior such as: + +- user-visible page and component behavior +- auth and routing behavior +- branching logic and business rules +- data normalization and error handling +- timing behavior with real app logic +- side effects that could regress, such as token handling or redirects +- algorithmic or formatting logic that defines product behavior + +Good examples in this repo: + +- login and registration flows +- protected-route behavior +- auth interceptor token injection and cleanup +- error message extraction +- debounce timing +- human-readable formatting logic +- graph/algorithm output where exact parity matters + +## What we usually do not want to test + +Avoid tests that mostly prove: + +- the language/runtime works +- React forwards basic props correctly +- a third-party library behaves as documented +- exact Tailwind class strings with no product meaning +- implementation details not observable in behavior +- duplicated examples that re-assert the same logic + +In other words, do not add tests equivalent to checking that JavaScript can compute `1 + 1`. + +## Preferred test style + +- Prefer behavior-focused assertions over implementation-detail assertions. +- Prefer user-visible outcomes over internal state inspection. +- Mock at module boundaries when needed. +- Keep test setup small and local. +- Use exact-output assertions only when the output itself is the contract. + +## When exact assertions are appropriate + +Exact assertions are appropriate when they protect a real contract, for example: + +- a formatter's exact human-readable output +- auth decision outcomes for a given API response shape +- exact algorithm output that must remain stable + +Exact assertions are usually not appropriate for: + +- incidental class names +- framework internals +- non-observable React keys + +## Cleanup rule of thumb + +Keep tests that would catch meaningful regressions in product behavior. +Trim or remove tests that are brittle, duplicated, or mostly validate tooling rather than app logic. + +## Validation + +For frontend test work, validate with: + +- `cd frontend && bun run lint` +- `cd frontend && bun run typecheck` +- `cd frontend && bun run test` diff --git a/frontend/bun.lock b/frontend/bun.lock new file mode 100644 index 0000000..1445a2a --- /dev/null +++ b/frontend/bun.lock @@ -0,0 +1,1350 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "antholume-frontend", + "dependencies": { + "@tanstack/react-query": "^5.62.16", + "ajv": "^8.18.0", + "axios": "^1.13.6", + "clsx": "^2.1.1", + "epubjs": "^0.3.93", + "nosleep.js": "^0.12.0", + "orval": "8.5.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.1.1", + "tailwind-merge": "^3.5.0", + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.8", + "@typescript-eslint/eslint-plugin": "^8.13.0", + "@typescript-eslint/parser": "^8.13.0", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.17.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-tailwindcss": "^3.18.2", + "jsdom": "^29.0.1", + "postcss": "^8.4.49", + "prettier": "^3.3.3", + "tailwindcss": "^3.4.17", + "typescript": "~5.6.2", + "vite": "^6.0.5", + "vitest": "^4.1.0", + }, + }, + }, + "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.6" } }, "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.4", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7" } }, "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], + + "@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.1", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + + "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="], + + "@gerrit0/mini-shiki": ["@gerrit0/mini-shiki@3.23.0", "", { "dependencies": { "@shikijs/engine-oniguruma": "^3.23.0", "@shikijs/langs": "^3.23.0", "@shikijs/themes": "^3.23.0", "@shikijs/types": "^3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@orval/angular": ["@orval/angular@8.5.3", "", { "dependencies": { "@orval/core": "8.5.3" } }, "sha512-0xzgPyZI+XbDVPsGVW2zTUkAK/xcloadfshI6T1KyVrmtPkCUbWnRFYF7w/IxRnoQ/WIXa2vMFTq04tXGWCGvw=="], + + "@orval/axios": ["@orval/axios@8.5.3", "", { "dependencies": { "@orval/core": "8.5.3" } }, "sha512-hUtcmofaKJKXWQ9FYmOR+4PqFlDGUZ2HdixCQmSLDJjL9os56P/IfdiW78hL8hr1oAWvcigG9oQce+rzzf6aCw=="], + + "@orval/core": ["@orval/core@8.5.3", "", { "dependencies": { "@scalar/openapi-types": "0.5.3", "acorn": "^8.15.0", "compare-versions": "^6.1.1", "debug": "^4.4.3", "esbuild": "^0.27.3", "esutils": "2.0.3", "fs-extra": "^11.3.2", "globby": "16.1.0", "remeda": "^2.33.6", "typedoc": "^0.28.17" }, "peerDependencies": { "@faker-js/faker": ">=10" }, "optionalPeers": ["@faker-js/faker"] }, "sha512-QGF2JfR58mGI+xACIOfkK9p9mfGBZ0iYNPkT35sMxya/Z1CLHEGt/MHqR/shpiLzayIC04B22IS/eiTElS7kiA=="], + + "@orval/fetch": ["@orval/fetch@8.5.3", "", { "dependencies": { "@orval/core": "8.5.3", "@scalar/openapi-types": "0.5.3" } }, "sha512-P+SuA44oqGu2UlT3wf8I9z9Zerfb9k/wHaqia6sAmH2q86mKpdDzAT5qvyKyrusZOiak3ijO92j2N7me3UzuFw=="], + + "@orval/hono": ["@orval/hono@8.5.3", "", { "dependencies": { "@orval/core": "8.5.3", "@orval/zod": "8.5.3", "fs-extra": "^11.3.2", "remeda": "^2.33.6" } }, "sha512-BZkjxq+5lwOnUtywHCXRGbzPrsjyZtBQ6bAwHXBkecNyUiZm/W1I8SUdD3KwCqBBT+bjs+aAZxGFk0FC8Cr1Rg=="], + + "@orval/mcp": ["@orval/mcp@8.5.3", "", { "dependencies": { "@orval/core": "8.5.3", "@orval/fetch": "8.5.3", "@orval/zod": "8.5.3" } }, "sha512-emd1fHrrcDgDnDH1k2dl6D2AAkPLRzX4K0ERtdp9SfFfLwa8NKVO3rdA3ZlYC1WxNUzOMEOM0ay2Pk+wEv/gaQ=="], + + "@orval/mock": ["@orval/mock@8.5.3", "", { "dependencies": { "@orval/core": "8.5.3", "remeda": "^2.33.6" } }, "sha512-wimpkpGhs6ZepQfOv6wej00IF+31H+zZVukFVsJMl7g+5mQjJ12M3+Kvhk7/GHAnV3jzXSaqPLF//m7vYgKSRQ=="], + + "@orval/query": ["@orval/query@8.5.3", "", { "dependencies": { "@orval/core": "8.5.3", "@orval/fetch": "8.5.3", "remeda": "^2.33.6" } }, "sha512-Q6YqlsVzQuxhJ3RWd4CWm1tdTsW1CjbYUR3QBvDN9SA5T6fZvhyD5Y7cflhOWaAuzZ/Y2a9aXRu8EpOjNlID1w=="], + + "@orval/solid-start": ["@orval/solid-start@8.5.3", "", { "dependencies": { "@orval/core": "8.5.3", "@scalar/openapi-types": "0.5.3" } }, "sha512-d9I+IUcXQ+sDGWYy5YmEZ4V9uAYabadMfGAz7FdxxnKyU3WzBL5PIyrGZHevZ23/9SPuWJ6bhXhiVggzhLSoAA=="], + + "@orval/swr": ["@orval/swr@8.5.3", "", { "dependencies": { "@orval/core": "8.5.3", "@orval/fetch": "8.5.3" } }, "sha512-2ScQVNnPjwVwuojUmJdy5LmFBeG2D/p2M2ONPL/w2IWTGLvJH7DxCnTaPMDO/4oc2EoSrySMLa8+5wDg6E503Q=="], + + "@orval/zod": ["@orval/zod@8.5.3", "", { "dependencies": { "@orval/core": "8.5.3", "remeda": "^2.33.6" } }, "sha512-qcbnpGE0VrgCDm0hNWQSOmzbfgdnr1xo+PYQ3PJjxfLuk3kGdJmFANTr53/1lI3sZUvWZwX5nKJCLWVxvwJEgg=="], + + "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@scalar/helpers": ["@scalar/helpers@0.2.18", "", {}, "sha512-w1d4tpNEVZ293oB2BAgLrS0kVPUtG3eByNmOCJA5eK9vcT4D3cmsGtWjUaaqit0BQCsBFHK51rasGvSWnApYTw=="], + + "@scalar/json-magic": ["@scalar/json-magic@0.11.7", "", { "dependencies": { "@scalar/helpers": "0.2.18", "pathe": "^2.0.3", "yaml": "^2.8.0" } }, "sha512-GVz9E0vXu+ecypkdn0biK1gbQVkK4QTTX1Hq3eMgxlLQC91wwiqWfCqwfhuX0LRu+Z5OmYhLhufDJEEh56rVgA=="], + + "@scalar/openapi-parser": ["@scalar/openapi-parser@0.24.17", "", { "dependencies": { "@scalar/helpers": "0.2.18", "@scalar/json-magic": "0.11.7", "@scalar/openapi-types": "0.5.4", "@scalar/openapi-upgrader": "0.1.11", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.8.0" } }, "sha512-aM9UVrzlMreC3X/sZbyj+7XDZmat3ecGC3RpU8dqEO/HIH+CEX0xMLuP+41DhePCYg5+9TtDomSfWuMq4x1Z1A=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.5.3", "", { "dependencies": { "zod": "^4.1.11" } }, "sha512-m4n/Su3K01d15dmdWO1LlqecdSPKuNjuokrJLdiQ485kW/hRHbXW1QP6tJL75myhw/XhX5YhYAR+jrwnGjXiMw=="], + + "@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.1.11", "", { "dependencies": { "@scalar/openapi-types": "0.5.4" } }, "sha512-ngJcHGoCHmpWgYtNy08vmzFfLdQEkMpvaCQqNPPMNKq0QEXOv89e/rn+TZJZgPnRlY7fDIoIhn9lNgr+azBW+w=="], + + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], + + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/localforage": ["@types/localforage@0.0.34", "", { "dependencies": { "localforage": "*" } }, "sha512-tJxahnjm9dEI1X+hQSC5f2BSd/coZaqbIl1m3TCl0q9SVuC52XcXfV0XmoCU1+PmjyucuVITwoTnN8OlTbEXXA=="], + + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + + "@vitest/expect": ["@vitest/expect@4.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.0", "", { "dependencies": { "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.0", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A=="], + + "@vitest/runner": ["@vitest/runner@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "pathe": "^2.0.3" } }, "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg=="], + + "@vitest/spy": ["@vitest/spy@4.1.0", "", {}, "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw=="], + + "@vitest/utils": ["@vitest/utils@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.7.13", "", {}, "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="], + + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001779", "", {}, "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], + + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "epubjs": ["epubjs@0.3.93", "", { "dependencies": { "@types/localforage": "0.0.34", "@xmldom/xmldom": "^0.7.5", "core-js": "^3.18.3", "event-emitter": "^0.3.5", "jszip": "^3.7.1", "localforage": "^1.10.0", "lodash": "^4.17.21", "marks-pane": "^1.0.9", "path-webpack": "0.0.3" } }, "sha512-c06pNSdBxcXv3dZSbXAVLE1/pmleRhOT6mXNZo6INKmvuKpYB65MwU/lO7830czCtjIiK9i+KR+3S+p0wtljrw=="], + + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-iterator-helpers": ["es-iterator-helpers@1.3.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0", "safe-array-concat": "^1.1.3" } }, "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ=="], + + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "es5-ext": ["es5-ext@0.10.64", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="], + + "es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="], + + "es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + + "eslint-config-prettier": ["eslint-config-prettier@9.1.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ=="], + + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.12" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + + "eslint-plugin-tailwindcss": ["eslint-plugin-tailwindcss@3.18.2", "", { "dependencies": { "fast-glob": "^3.2.5", "postcss": "^8.4.4" }, "peerDependencies": { "tailwindcss": "^3.4.0" } }, "sha512-QbkMLDC/OkkjFQ1iz/5jkMdHfiMu/uwujUHLAJK5iwNHD8RTxVTlsUezE0toTZ6VhybNBsk+gYGPDq2agfeRNA=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="], + + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "globby": ["globby@16.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", "is-path-inside": "^4.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.4.0" } }, "sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsdom": ["jsdom@29.0.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.0.1", "@asamuzakjp/dom-selector": "^7.0.3", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "leven": ["leven@4.1.0", "", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + + "localforage": ["localforage@1.10.0", "", { "dependencies": { "lie": "3.1.1" } }, "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], + + "lunr": ["lunr@2.3.9", "", {}, "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], + + "marks-pane": ["marks-pane@1.0.9", "", {}, "sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="], + + "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], + + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "nosleep.js": ["nosleep.js@0.12.0", "", {}, "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA=="], + + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "orval": ["orval@8.5.3", "", { "dependencies": { "@commander-js/extra-typings": "^14.0.0", "@orval/angular": "8.5.3", "@orval/axios": "8.5.3", "@orval/core": "8.5.3", "@orval/fetch": "8.5.3", "@orval/hono": "8.5.3", "@orval/mcp": "8.5.3", "@orval/mock": "8.5.3", "@orval/query": "8.5.3", "@orval/solid-start": "8.5.3", "@orval/swr": "8.5.3", "@orval/zod": "8.5.3", "@scalar/json-magic": "^0.11.5", "@scalar/openapi-parser": "^0.24.13", "@scalar/openapi-types": "0.5.3", "chokidar": "^5.0.0", "commander": "^14.0.2", "enquirer": "^2.4.1", "execa": "^9.6.1", "find-up": "8.0.0", "fs-extra": "^11.3.2", "jiti": "^2.6.1", "js-yaml": "4.1.1", "remeda": "^2.33.6", "string-argv": "^0.3.2", "tsconfck": "^3.1.6", "typedoc": "^0.28.17", "typedoc-plugin-coverage": "^4.0.2", "typedoc-plugin-markdown": "^4.10.0" }, "peerDependencies": { "prettier": ">=3.0.0" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/orval.mjs" }, "sha512-+8Es2ZR3tPthzAL27X1a9AlboqTQ/w9U/PhMkp4vsLA9OvdkpXr+9f8lCfJUV/wtdX+lXBDQ4imx42Em943JSg=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + + "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-webpack": ["path-webpack@0.0.3", "", {}, "sha512-AmeDxedoo5svf7aB3FYqSAKqMxys014lVKBzy1o/5vv9CtU7U4wgGWL1dA2o6MOzcD53ScN4Jmiq6VbtLz1vIQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], + + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "react-router": ["react-router@7.13.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA=="], + + "react-router-dom": ["react-router-dom@7.13.1", "", { "dependencies": { "react-router": "7.13.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "remeda": ["remeda@2.33.6", "", {}, "sha512-tazDGH7s75kUPGBKLvhgBEHMgW+TdDFhjUAMdQj57IoWz6HsGa5D2RX5yDUz6IIqiRRvZiaEHzCzWdTeixc/Kg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="], + + "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], + + "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "tldts": ["tldts@7.0.27", "", { "dependencies": { "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg=="], + + "tldts-core": ["tldts-core@7.0.27", "", {}, "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], + + "type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typedoc": ["typedoc@0.28.17", "", { "dependencies": { "@gerrit0/mini-shiki": "^3.17.0", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", "yaml": "^2.8.1" }, "peerDependencies": { "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" }, "bin": { "typedoc": "bin/typedoc" } }, "sha512-ZkJ2G7mZrbxrKxinTQMjFqsCoYY6a5Luwv2GKbTnBCEgV2ihYm5CflA9JnJAwH0pZWavqfYxmDkFHPt4yx2oDQ=="], + + "typedoc-plugin-coverage": ["typedoc-plugin-coverage@4.0.2", "", { "peerDependencies": { "typedoc": "0.28.x" } }, "sha512-mfn0e7NCqB8x2PfvhXrtmd7KWlsNf1+B2N9y8gR/jexXBLrXl/0e+b2HdG5HaTXGi7i0t2pyQY2VRmq7gtdEHQ=="], + + "typedoc-plugin-markdown": ["typedoc-plugin-markdown@4.10.0", "", { "peerDependencies": { "typedoc": "0.28.x" } }, "sha512-psrg8Rtnv4HPWCsoxId+MzEN8TVK5jeKCnTbnGAbTBqcDapR9hM41bJT/9eAyKn9C2MDG9Qjh3MkltAYuLDoXg=="], + + "typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], + + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "undici": ["undici@7.24.5", "", {}, "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + + "vitest": ["vitest@4.1.0", "", { "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", "@vitest/pretty-format": "4.1.0", "@vitest/runner": "4.1.0", "@vitest/snapshot": "4.1.0", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.0", "@vitest/browser-preview": "4.1.0", "@vitest/browser-webdriverio": "4.1.0", "@vitest/ui": "4.1.0", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@orval/core/esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], + + "@scalar/openapi-parser/@scalar/openapi-types": ["@scalar/openapi-types@0.5.4", "", { "dependencies": { "zod": "^4.3.5" } }, "sha512-2pEbhprh8lLGDfUI6mNm9EV104pjb3+aJsXrFaqfgOSre7r6NlgM5HcSbsLjzDAnTikjJhJ3IMal1Rz8WVwiOw=="], + + "@scalar/openapi-upgrader/@scalar/openapi-types": ["@scalar/openapi-types@0.5.4", "", { "dependencies": { "zod": "^4.3.5" } }, "sha512-2pEbhprh8lLGDfUI6mNm9EV104pjb3+aJsXrFaqfgOSre7r6NlgM5HcSbsLjzDAnTikjJhJ3IMal1Rz8WVwiOw=="], + + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "eslint/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globby/unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="], + + "localforage/lie": ["lie@3.1.1", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw=="], + + "markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "orval/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "orval/find-up": ["find-up@8.0.0", "", { "dependencies": { "locate-path": "^8.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww=="], + + "postcss-import/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "tailwindcss/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "typedoc/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "@orval/core/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="], + + "@orval/core/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="], + + "@orval/core/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="], + + "@orval/core/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="], + + "@orval/core/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="], + + "@orval/core/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="], + + "@orval/core/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="], + + "@orval/core/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="], + + "@orval/core/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="], + + "@orval/core/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="], + + "@orval/core/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="], + + "@orval/core/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="], + + "@orval/core/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="], + + "@orval/core/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="], + + "@orval/core/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="], + + "@orval/core/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="], + + "@orval/core/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="], + + "@orval/core/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="], + + "@orval/core/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="], + + "@orval/core/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="], + + "@orval/core/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="], + + "@orval/core/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="], + + "@orval/core/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="], + + "@orval/core/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="], + + "@orval/core/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="], + + "@orval/core/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "orval/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "orval/find-up/locate-path": ["locate-path@8.0.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg=="], + + "typedoc/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "orval/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + + "orval/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + + "orval/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + } +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..9f6ed4e --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,82 @@ +import js from "@eslint/js"; +import typescriptParser from "@typescript-eslint/parser"; +import typescriptPlugin from "@typescript-eslint/eslint-plugin"; +import reactPlugin from "eslint-plugin-react"; +import reactHooksPlugin from "eslint-plugin-react-hooks"; +import tailwindcss from "eslint-plugin-tailwindcss"; +import prettier from "eslint-plugin-prettier"; +import eslintConfigPrettier from "eslint-config-prettier"; + +export default [ + js.configs.recommended, + { + files: ["**/*.ts", "**/*.tsx"], + ignores: ["**/generated/**"], + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + projectService: true, + }, + globals: { + localStorage: "readonly", + sessionStorage: "readonly", + document: "readonly", + window: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + HTMLElement: "readonly", + HTMLDivElement: "readonly", + HTMLButtonElement: "readonly", + HTMLAnchorElement: "readonly", + MouseEvent: "readonly", + Node: "readonly", + File: "readonly", + Blob: "readonly", + FormData: "readonly", + alert: "readonly", + confirm: "readonly", + prompt: "readonly", + React: "readonly", + }, + }, + plugins: { + "@typescript-eslint": typescriptPlugin, + react: reactPlugin, + "react-hooks": reactHooksPlugin, + tailwindcss, + prettier, + }, + rules: { + ...eslintConfigPrettier.rules, + ...tailwindcss.configs.recommended.rules, + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "no-console": ["warn", { allow: ["warn", "error"] }], + "no-undef": "off", + "@typescript-eslint/no-explicit-any": "warn", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + "no-useless-catch": "off", + }, + settings: { + react: { + version: "detect", + }, + }, + }, +]; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..fcb0bca --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + AnthoLume + + + +
+ + + \ No newline at end of file diff --git a/frontend/orval.config.ts b/frontend/orval.config.ts new file mode 100644 index 0000000..7cbd58b --- /dev/null +++ b/frontend/orval.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'orval'; + +export default defineConfig({ + antholume: { + output: { + mode: 'split', + baseUrl: '/api/v1', + target: 'src/generated', + schemas: 'src/generated/model', + client: 'react-query', + mock: false, + override: { + useQuery: true, + mutations: true, + }, + }, + input: { + target: '../api/v1/openapi.yaml', + }, + }, +}); \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..ceb1634 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,56 @@ +{ + "name": "antholume-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "typecheck": "tsc --noEmit", + "build": "tsc && vite build", + "preview": "vite preview", + "generate:api": "orval", + "lint": "eslint src --max-warnings=0", + "lint:fix": "eslint src --fix", + "format": "prettier --check src", + "format:fix": "prettier --write src", + "test": "vitest run" + }, + "dependencies": { + "@tanstack/react-query": "^5.62.16", + "ajv": "^8.18.0", + "axios": "^1.13.6", + "clsx": "^2.1.1", + "epubjs": "^0.3.93", + "nosleep.js": "^0.12.0", + "orval": "8.5.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.1.1", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.8", + "@typescript-eslint/eslint-plugin": "^8.13.0", + "@typescript-eslint/parser": "^8.13.0", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.17.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-tailwindcss": "^3.18.2", + "jsdom": "^29.0.1", + "postcss": "^8.4.49", + "prettier": "^3.3.3", + "tailwindcss": "^3.4.17", + "typescript": "~5.6.2", + "vite": "^6.0.5", + "vitest": "^4.1.0" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..fbe14a4 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..9c34c63 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,12 @@ +import { AuthProvider } from './auth/AuthContext'; +import { Routes } from './Routes'; + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/frontend/src/Routes.tsx b/frontend/src/Routes.tsx new file mode 100644 index 0000000..f7ec262 --- /dev/null +++ b/frontend/src/Routes.tsx @@ -0,0 +1,134 @@ +import { Route, Routes as ReactRoutes } from 'react-router-dom'; +import Layout from './components/Layout'; +import HomePage from './pages/HomePage'; +import DocumentsPage from './pages/DocumentsPage'; +import DocumentPage from './pages/DocumentPage'; +import ProgressPage from './pages/ProgressPage'; +import ActivityPage from './pages/ActivityPage'; +import SearchPage from './pages/SearchPage'; +import SettingsPage from './pages/SettingsPage'; +import LoginPage from './pages/LoginPage'; +import RegisterPage from './pages/RegisterPage'; +import AdminPage from './pages/AdminPage'; +import AdminImportPage from './pages/AdminImportPage'; +import AdminImportResultsPage from './pages/AdminImportResultsPage'; +import AdminUsersPage from './pages/AdminUsersPage'; +import AdminLogsPage from './pages/AdminLogsPage'; +import ReaderPage from './pages/ReaderPage'; +import { ProtectedRoute } from './auth/ProtectedRoute'; + +export function Routes() { + return ( + + }> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + {/* Admin routes */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + + } + /> + } /> + } /> + + ); +} diff --git a/frontend/src/auth/AuthContext.tsx b/frontend/src/auth/AuthContext.tsx new file mode 100644 index 0000000..d6d9c51 --- /dev/null +++ b/frontend/src/auth/AuthContext.tsx @@ -0,0 +1,135 @@ +import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import { + getGetMeQueryKey, + useLogin, + useLogout, + useGetMe, + useRegister, +} from '../generated/anthoLumeAPIV1'; +import { + type AuthState, + getAuthenticatedAuthState, + getUnauthenticatedAuthState, + resolveAuthStateFromMe, + validateAuthMutationResponse, +} from './authHelpers'; + +interface AuthContextType extends AuthState { + login: (_username: string, _password: string) => Promise; + register: (_username: string, _password: string) => Promise; + logout: () => void; +} + +const AuthContext = createContext(undefined); + +const initialAuthState: AuthState = { + isAuthenticated: false, + user: null, + isCheckingAuth: true, +}; + +export function AuthProvider({ children }: { children: ReactNode }) { + const [authState, setAuthState] = useState(initialAuthState); + + const loginMutation = useLogin(); + const registerMutation = useRegister(); + const logoutMutation = useLogout(); + + const { data: meData, error: meError, isLoading: meLoading } = useGetMe(); + + const queryClient = useQueryClient(); + const navigate = useNavigate(); + + useEffect(() => { + setAuthState(prev => + resolveAuthStateFromMe({ + meData, + meError, + meLoading, + previousState: prev, + }) + ); + }, [meData, meError, meLoading]); + + const login = useCallback( + async (username: string, password: string) => { + try { + const response = await loginMutation.mutateAsync({ + data: { + username, + password, + }, + }); + + const user = validateAuthMutationResponse(response, 200); + if (!user) { + setAuthState(getUnauthenticatedAuthState()); + throw new Error('Login failed'); + } + + setAuthState(getAuthenticatedAuthState(user)); + + await queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() }); + navigate('/'); + } catch (_error) { + setAuthState(getUnauthenticatedAuthState()); + throw new Error('Login failed'); + } + }, + [loginMutation, navigate, queryClient] + ); + + const register = useCallback( + async (username: string, password: string) => { + try { + const response = await registerMutation.mutateAsync({ + data: { + username, + password, + }, + }); + + const user = validateAuthMutationResponse(response, 201); + if (!user) { + setAuthState(getUnauthenticatedAuthState()); + throw new Error('Registration failed'); + } + + setAuthState(getAuthenticatedAuthState(user)); + + await queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() }); + navigate('/'); + } catch (_error) { + setAuthState(getUnauthenticatedAuthState()); + throw new Error('Registration failed'); + } + }, + [navigate, queryClient, registerMutation] + ); + + const logout = useCallback(() => { + logoutMutation.mutate(undefined, { + onSuccess: async () => { + setAuthState(getUnauthenticatedAuthState()); + await queryClient.removeQueries({ queryKey: getGetMeQueryKey() }); + navigate('/login'); + }, + }); + }, [logoutMutation, navigate, queryClient]); + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} diff --git a/frontend/src/auth/ProtectedRoute.test.tsx b/frontend/src/auth/ProtectedRoute.test.tsx new file mode 100644 index 0000000..52aa2ca --- /dev/null +++ b/frontend/src/auth/ProtectedRoute.test.tsx @@ -0,0 +1,90 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { ProtectedRoute } from './ProtectedRoute'; +import { useAuth } from './AuthContext'; + +vi.mock('./AuthContext', () => ({ + useAuth: vi.fn(), +})); + +const mockedUseAuth = vi.mocked(useAuth); + +describe('ProtectedRoute', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('shows a loading state while auth is being checked', () => { + mockedUseAuth.mockReturnValue({ + isAuthenticated: false, + isCheckingAuth: true, + user: null, + login: vi.fn(), + register: vi.fn(), + logout: vi.fn(), + }); + + render( + + +
Secret
+
+
+ ); + + expect(screen.getByText('Loading...')).toBeInTheDocument(); + expect(screen.queryByText('Secret')).not.toBeInTheDocument(); + }); + + it('redirects unauthenticated users to the login page', () => { + mockedUseAuth.mockReturnValue({ + isAuthenticated: false, + isCheckingAuth: false, + user: null, + login: vi.fn(), + register: vi.fn(), + logout: vi.fn(), + }); + + render( + + + +
Secret
+ + } + /> + Login Page} /> +
+
+ ); + + expect(screen.getByText('Login Page')).toBeInTheDocument(); + expect(screen.queryByText('Secret')).not.toBeInTheDocument(); + }); + + it('renders children for authenticated users', () => { + mockedUseAuth.mockReturnValue({ + isAuthenticated: true, + isCheckingAuth: false, + user: { username: 'evan', is_admin: false }, + login: vi.fn(), + register: vi.fn(), + logout: vi.fn(), + }); + + render( + + +
Secret
+
+
+ ); + + expect(screen.getByText('Secret')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/auth/ProtectedRoute.tsx b/frontend/src/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..cf3db8a --- /dev/null +++ b/frontend/src/auth/ProtectedRoute.tsx @@ -0,0 +1,21 @@ +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from './AuthContext'; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export function ProtectedRoute({ children }: ProtectedRouteProps) { + const { isAuthenticated, isCheckingAuth } = useAuth(); + const location = useLocation(); + + if (isCheckingAuth) { + return
Loading...
; + } + + if (!isAuthenticated) { + return ; + } + + return children; +} diff --git a/frontend/src/auth/authHelpers.test.ts b/frontend/src/auth/authHelpers.test.ts new file mode 100644 index 0000000..13fd032 --- /dev/null +++ b/frontend/src/auth/authHelpers.test.ts @@ -0,0 +1,157 @@ +import { describe, expect, it } from 'vitest'; +import { + getCheckingAuthState, + getUnauthenticatedAuthState, + normalizeAuthenticatedUser, + resolveAuthStateFromMe, + validateAuthMutationResponse, + type AuthState, +} from './authHelpers'; + +const previousState: AuthState = { + isAuthenticated: false, + user: null, + isCheckingAuth: true, +}; + +describe('authHelpers', () => { + it('normalizes a valid authenticated user payload', () => { + expect(normalizeAuthenticatedUser({ username: 'evan', is_admin: true })).toEqual({ + username: 'evan', + is_admin: true, + }); + }); + + it('rejects invalid authenticated user payloads', () => { + expect(normalizeAuthenticatedUser(null)).toBeNull(); + expect(normalizeAuthenticatedUser({ username: 'evan' })).toBeNull(); + expect(normalizeAuthenticatedUser({ username: 123, is_admin: true })).toBeNull(); + expect(normalizeAuthenticatedUser({ username: 'evan', is_admin: 'yes' })).toBeNull(); + }); + + it('returns a checking state while preserving previous auth information', () => { + expect( + getCheckingAuthState({ + isAuthenticated: true, + user: { username: 'evan', is_admin: false }, + isCheckingAuth: false, + }) + ).toEqual({ + isAuthenticated: true, + user: { username: 'evan', is_admin: false }, + isCheckingAuth: true, + }); + }); + + it('resolves auth state from a successful /auth/me response', () => { + expect( + resolveAuthStateFromMe({ + meData: { + status: 200, + data: { username: 'evan', is_admin: false }, + }, + meError: undefined, + meLoading: false, + previousState, + }) + ).toEqual({ + isAuthenticated: true, + user: { username: 'evan', is_admin: false }, + isCheckingAuth: false, + }); + }); + + it('resolves auth state to unauthenticated on 401 or query error', () => { + expect( + resolveAuthStateFromMe({ + meData: { + status: 401, + }, + meError: undefined, + meLoading: false, + previousState, + }) + ).toEqual(getUnauthenticatedAuthState()); + + expect( + resolveAuthStateFromMe({ + meData: undefined, + meError: new Error('failed'), + meLoading: false, + previousState, + }) + ).toEqual(getUnauthenticatedAuthState()); + }); + + it('keeps checking state while /auth/me is still loading', () => { + expect( + resolveAuthStateFromMe({ + meData: undefined, + meError: undefined, + meLoading: true, + previousState: { + isAuthenticated: true, + user: { username: 'evan', is_admin: true }, + isCheckingAuth: false, + }, + }) + ).toEqual({ + isAuthenticated: true, + user: { username: 'evan', is_admin: true }, + isCheckingAuth: true, + }); + }); + + it('returns the previous state with checking disabled when there is no decisive me result', () => { + expect( + resolveAuthStateFromMe({ + meData: { + status: 204, + }, + meError: undefined, + meLoading: false, + previousState: { + isAuthenticated: false, + user: null, + isCheckingAuth: true, + }, + }) + ).toEqual({ + isAuthenticated: false, + user: null, + isCheckingAuth: false, + }); + }); + + it('validates auth mutation responses by expected status and payload shape', () => { + expect( + validateAuthMutationResponse( + { + status: 200, + data: { username: 'evan', is_admin: false }, + }, + 200 + ) + ).toEqual({ username: 'evan', is_admin: false }); + + expect( + validateAuthMutationResponse( + { + status: 201, + data: { username: 'evan', is_admin: false }, + }, + 200 + ) + ).toBeNull(); + + expect( + validateAuthMutationResponse( + { + status: 200, + data: { username: 'evan' }, + }, + 200 + ) + ).toBeNull(); + }); +}); diff --git a/frontend/src/auth/authHelpers.ts b/frontend/src/auth/authHelpers.ts new file mode 100644 index 0000000..b403cea --- /dev/null +++ b/frontend/src/auth/authHelpers.ts @@ -0,0 +1,98 @@ +export interface AuthUser { + username: string; + is_admin: boolean; +} + +export interface AuthState { + isAuthenticated: boolean; + user: AuthUser | null; + isCheckingAuth: boolean; +} + +interface ResponseLike { + status?: number; + data?: unknown; +} + +export function getUnauthenticatedAuthState(): AuthState { + return { + isAuthenticated: false, + user: null, + isCheckingAuth: false, + }; +} + +export function getCheckingAuthState(previousState?: AuthState): AuthState { + return { + isAuthenticated: previousState?.isAuthenticated ?? false, + user: previousState?.user ?? null, + isCheckingAuth: true, + }; +} + +export function getAuthenticatedAuthState(user: AuthUser): AuthState { + return { + isAuthenticated: true, + user, + isCheckingAuth: false, + }; +} + +export function normalizeAuthenticatedUser(value: unknown): AuthUser | null { + if (!value || typeof value !== 'object') { + return null; + } + + if (!('username' in value) || typeof value.username !== 'string') { + return null; + } + + if (!('is_admin' in value) || typeof value.is_admin !== 'boolean') { + return null; + } + + return { + username: value.username, + is_admin: value.is_admin, + }; +} + +export function resolveAuthStateFromMe(params: { + meData?: ResponseLike; + meError?: unknown; + meLoading: boolean; + previousState: AuthState; +}): AuthState { + const { meData, meError, meLoading, previousState } = params; + + if (meLoading) { + return getCheckingAuthState(previousState); + } + + if (meData?.status === 200) { + const user = normalizeAuthenticatedUser(meData.data); + if (user) { + return getAuthenticatedAuthState(user); + } + } + + if (meError || meData?.status === 401) { + return getUnauthenticatedAuthState(); + } + + return { + ...previousState, + isCheckingAuth: false, + }; +} + +export function validateAuthMutationResponse( + response: ResponseLike, + expectedStatus: number +): AuthUser | null { + if (response.status !== expectedStatus) { + return null; + } + + return normalizeAuthenticatedUser(response.data); +} diff --git a/frontend/src/auth/authInterceptor.test.ts b/frontend/src/auth/authInterceptor.test.ts new file mode 100644 index 0000000..de23385 --- /dev/null +++ b/frontend/src/auth/authInterceptor.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; +import { setupAuthInterceptors } from './authInterceptor'; + +describe('setupAuthInterceptors', () => { + it('is a no-op when auth is handled by HttpOnly cookies', () => { + const cleanup = setupAuthInterceptors(); + + expect(typeof cleanup).toBe('function'); + expect(() => cleanup()).not.toThrow(); + }); +}); diff --git a/frontend/src/auth/authInterceptor.ts b/frontend/src/auth/authInterceptor.ts new file mode 100644 index 0000000..6a764c0 --- /dev/null +++ b/frontend/src/auth/authInterceptor.ts @@ -0,0 +1,3 @@ +export function setupAuthInterceptors() { + return () => {}; +} diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx new file mode 100644 index 0000000..0621291 --- /dev/null +++ b/frontend/src/components/Button.tsx @@ -0,0 +1,45 @@ +import { ButtonHTMLAttributes, AnchorHTMLAttributes, forwardRef } from 'react'; + +interface BaseButtonProps { + variant?: 'default' | 'secondary'; + children: React.ReactNode; + className?: string; +} + +type ButtonProps = BaseButtonProps & ButtonHTMLAttributes; +type LinkProps = BaseButtonProps & AnchorHTMLAttributes & { href: string }; + +const getVariantClasses = (variant: 'default' | 'secondary' = 'default'): string => { + const baseClass = + 'h-full w-full px-2 py-1 font-medium transition duration-100 ease-in disabled:cursor-not-allowed disabled:opacity-50'; + + if (variant === 'secondary') { + return `${baseClass} bg-content text-content-inverse shadow-md hover:bg-content-muted disabled:hover:bg-content`; + } + + return `${baseClass} bg-primary-500 text-primary-foreground hover:bg-primary-700 disabled:hover:bg-primary-500`; +}; + +export const Button = forwardRef( + ({ variant = 'default', children, className = '', ...props }, ref) => { + return ( + + ); + } +); + +Button.displayName = 'Button'; + +export const ButtonLink = forwardRef( + ({ variant = 'default', children, className = '', ...props }, ref) => { + return ( + + {children} + + ); + } +); + +ButtonLink.displayName = 'ButtonLink'; diff --git a/frontend/src/components/Field.tsx b/frontend/src/components/Field.tsx new file mode 100644 index 0000000..589d81a --- /dev/null +++ b/frontend/src/components/Field.tsx @@ -0,0 +1,41 @@ +import { ReactNode } from 'react'; + +interface FieldProps { + label: ReactNode; + children: ReactNode; + isEditing?: boolean; +} + +export function Field({ label, children, isEditing: _isEditing = false }: FieldProps) { + return ( +
+
{label}
+ {children} +
+ ); +} + +interface FieldLabelProps { + children: ReactNode; +} + +export function FieldLabel({ children }: FieldLabelProps) { + return

{children}

; +} + +interface FieldValueProps { + children: ReactNode; + className?: string; +} + +export function FieldValue({ children, className = '' }: FieldValueProps) { + return

{children}

; +} + +interface FieldActionsProps { + children: ReactNode; +} + +export function FieldActions({ children }: FieldActionsProps) { + return
{children}
; +} diff --git a/frontend/src/components/HamburgerMenu.tsx b/frontend/src/components/HamburgerMenu.tsx new file mode 100644 index 0000000..bc7a663 --- /dev/null +++ b/frontend/src/components/HamburgerMenu.tsx @@ -0,0 +1,181 @@ +import { useState } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { HomeIcon, DocumentsIcon, ActivityIcon, SearchIcon, SettingsIcon, GitIcon } from '../icons'; +import { useAuth } from '../auth/AuthContext'; +import { useGetInfo } from '../generated/anthoLumeAPIV1'; + +interface NavItem { + path: string; + label: string; + icon: React.ElementType; + title: string; +} + +const navItems: NavItem[] = [ + { path: '/', label: 'Home', icon: HomeIcon, title: 'Home' }, + { path: '/documents', label: 'Documents', icon: DocumentsIcon, title: 'Documents' }, + { path: '/progress', label: 'Progress', icon: ActivityIcon, title: 'Progress' }, + { path: '/activity', label: 'Activity', icon: ActivityIcon, title: 'Activity' }, + { path: '/search', label: 'Search', icon: SearchIcon, title: 'Search' }, +]; + +const adminSubItems: NavItem[] = [ + { path: '/admin', label: 'General', icon: SettingsIcon, title: 'General' }, + { path: '/admin/import', label: 'Import', icon: SettingsIcon, title: 'Import' }, + { path: '/admin/users', label: 'Users', icon: SettingsIcon, title: 'Users' }, + { path: '/admin/logs', label: 'Logs', icon: SettingsIcon, title: 'Logs' }, +]; + +function hasPrefix(path: string, prefix: string): boolean { + return path.startsWith(prefix); +} + +export default function HamburgerMenu() { + const location = useLocation(); + const { user } = useAuth(); + const [isOpen, setIsOpen] = useState(false); + const isAdmin = user?.is_admin ?? false; + + const { data: infoData } = useGetInfo({ + query: { + staleTime: Infinity, + }, + }); + const version = + infoData && 'data' in infoData && infoData.data && 'version' in infoData.data + ? infoData.data.version + : 'v1.0.0'; + + return ( +
+ setIsOpen(e.target.checked)} + /> + + + + + + +
+ ); +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..c4b8eca --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,178 @@ +import { useState, useEffect, useRef } from 'react'; +import { Link, useLocation, Outlet, Navigate } from 'react-router-dom'; +import { useGetMe } from '../generated/anthoLumeAPIV1'; +import { useAuth } from '../auth/AuthContext'; +import { UserIcon, DropdownIcon } from '../icons'; +import { useTheme } from '../theme/ThemeProvider'; +import type { ThemeMode } from '../utils/localSettings'; +import HamburgerMenu from './HamburgerMenu'; + +const themeModes: ThemeMode[] = ['light', 'dark', 'system']; + +export default function Layout() { + const location = useLocation(); + const { isAuthenticated, user, logout, isCheckingAuth } = useAuth(); + const { themeMode, setThemeMode } = useTheme(); + const { data } = useGetMe(isAuthenticated ? {} : undefined); + const fetchedUser = + data?.status === 200 && data.data && 'username' in data.data ? data.data : null; + const userData = user ?? fetchedUser; + const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + const handleLogout = () => { + logout(); + setIsUserDropdownOpen(false); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsUserDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const navItems = [ + { path: '/admin/import-results', title: 'Admin - Import' }, + { path: '/admin/import', title: 'Admin - Import' }, + { path: '/admin/users', title: 'Admin - Users' }, + { path: '/admin/logs', title: 'Admin - Logs' }, + { path: '/admin', title: 'Admin - General' }, + { path: '/documents', title: 'Documents' }, + { path: '/progress', title: 'Progress' }, + { path: '/activity', title: 'Activity' }, + { path: '/search', title: 'Search' }, + { path: '/settings', title: 'Settings' }, + { path: '/', title: 'Home' }, + ]; + const currentPageTitle = + navItems.find(item => + item.path === '/' ? location.pathname === item.path : location.pathname.startsWith(item.path) + )?.title || 'Home'; + + useEffect(() => { + document.title = `AnthoLume - ${currentPageTitle}`; + }, [currentPageTitle]); + + if (isCheckingAuth) { + return
Loading...
; + } + + if (!isAuthenticated) { + return ; + } + + return ( +
+
+ + +

+ {currentPageTitle} +

+ +
+ + + {isUserDropdownOpen && ( +
+
+
+

+ Theme +

+
+ {themeModes.map(mode => ( + + ))} +
+
+
+ setIsUserDropdownOpen(false)} + className="block px-4 py-2 text-content-muted hover:bg-surface-muted hover:text-content" + role="menuitem" + > + + Settings + + + +
+
+
+ )} + + +
+
+ +
+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/LoadingState.tsx b/frontend/src/components/LoadingState.tsx new file mode 100644 index 0000000..7a761ef --- /dev/null +++ b/frontend/src/components/LoadingState.tsx @@ -0,0 +1,21 @@ +import { LoadingIcon } from '../icons'; +import { cn } from '../utils/cn'; + +interface LoadingStateProps { + message?: string; + className?: string; + iconSize?: number; +} + +export function LoadingState({ + message = 'Loading...', + className = '', + iconSize = 24, +}: LoadingStateProps) { + return ( +
+ + {message} +
+ ); +} diff --git a/frontend/src/components/README.md b/frontend/src/components/README.md new file mode 100644 index 0000000..8bbeed2 --- /dev/null +++ b/frontend/src/components/README.md @@ -0,0 +1,203 @@ +# UI Components + +This directory contains reusable UI components for the AnthoLume application. + +## Toast Notifications + +### Usage + +The toast system provides info, warning, and error notifications that respect the current theme and dark/light mode. + +```tsx +import { useToasts } from './components/ToastContext'; + +function MyComponent() { + const { showInfo, showWarning, showError, showToast } = useToasts(); + + const handleAction = async () => { + try { + // Do something + showInfo('Operation completed successfully!'); + } catch (error) { + showError('An error occurred while processing your request.'); + } + }; + + return ; +} +``` + +### API + +- `showToast(message: string, type?: 'info' | 'warning' | 'error', duration?: number): string` + - Shows a toast notification + - Returns the toast ID for manual removal + - Default type: 'info' + - Default duration: 5000ms (0 = no auto-dismiss) + +- `showInfo(message: string, duration?: number): string` + - Shortcut for showing an info toast + +- `showWarning(message: string, duration?: number): string` + - Shortcut for showing a warning toast + +- `showError(message: string, duration?: number): string` + - Shortcut for showing an error toast + +- `removeToast(id: string): void` + - Manually remove a toast by ID + +- `clearToasts(): void` + - Clear all active toasts + +### Examples + +```tsx +// Info toast (auto-dismisses after 5 seconds) +showInfo('Document saved successfully!'); + +// Warning toast (auto-dismisses after 10 seconds) +showWarning('Low disk space warning', 10000); + +// Error toast (no auto-dismiss) +showError('Failed to load data', 0); + +// Generic toast +showToast('Custom message', 'warning', 3000); +``` + +## Skeleton Loading + +### Usage + +Skeleton components provide placeholder content while data is loading. They automatically adapt to dark/light mode. + +### Components + +#### `Skeleton` + +Basic skeleton element with various variants: + +```tsx +import { Skeleton } from './components/Skeleton'; + +// Default (rounded rectangle) + + +// Text variant + + +// Circular variant (for avatars) + + +// Rectangular variant + +``` + +#### `SkeletonText` + +Multiple lines of text skeleton: + +```tsx + + +``` + +#### `SkeletonAvatar` + +Avatar placeholder: + +```tsx + + +``` + +#### `SkeletonCard` + +Card placeholder with optional elements: + +```tsx +// Default card + + +// With avatar + + +// Custom configuration + +``` + +#### `SkeletonTable` + +Table placeholder: + +```tsx + + +``` + +#### `SkeletonButton` + +Button placeholder: + +```tsx + + +``` + +#### `PageLoader` + +Full-page loading indicator: + +```tsx + +``` + +#### `InlineLoader` + +Small inline loading spinner: + +```tsx + + + +``` + +## Integration with Table Component + +The Table component now supports skeleton loading: + +```tsx +import { Table, SkeletonTable } from './components/Table'; + +function DocumentList() { + const { data, isLoading } = useGetDocuments(); + + if (isLoading) { + return ; + } + + return ; +} +``` + +## Theme Support + +All components automatically adapt to the current theme: + +- **Light mode**: Uses gray tones for skeletons, appropriate colors for toasts +- **Dark mode**: Uses darker gray tones for skeletons, adjusted colors for toasts + +The theme is controlled via Tailwind's `dark:` classes, which respond to the system preference or manual theme toggles. + +## Dependencies + +- `clsx` - Utility for constructing className strings +- `tailwind-merge` - Merges Tailwind CSS classes intelligently +- `lucide-react` - Icon library used by Toast component diff --git a/frontend/src/components/ReadingHistoryGraph.test.ts b/frontend/src/components/ReadingHistoryGraph.test.ts new file mode 100644 index 0000000..c8e508a --- /dev/null +++ b/frontend/src/components/ReadingHistoryGraph.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; +import { getSVGGraphData } from './ReadingHistoryGraph'; + +// Intentionally exact fixture data for algorithm parity coverage +const testInput = [ + { date: '2024-01-01', minutes_read: 10 }, + { date: '2024-01-02', minutes_read: 90 }, + { date: '2024-01-03', minutes_read: 50 }, + { date: '2024-01-04', minutes_read: 5 }, + { date: '2024-01-05', minutes_read: 10 }, + { date: '2024-01-06', minutes_read: 5 }, + { date: '2024-01-07', minutes_read: 70 }, + { date: '2024-01-08', minutes_read: 60 }, + { date: '2024-01-09', minutes_read: 50 }, + { date: '2024-01-10', minutes_read: 90 }, +]; + +const svgWidth = 500; +const svgHeight = 100; + +describe('ReadingHistoryGraph', () => { + describe('getSVGGraphData', () => { + it('should match exactly', () => { + const result = getSVGGraphData(testInput, svgWidth, svgHeight); + + // Expected exact algorithm output + const expectedBezierPath = + 'M 50,95 C63,95 80,50 100,50 C120,50 128,73 150,73 C172,73 180,98 200,98 C220,98 230,95 250,95 C270,95 279,98 300,98 C321,98 330,62 350,62 C370,62 380,67 400,67 C420,67 430,73 450,73 C470,73 489,50 500,50'; + const expectedBezierFill = 'L 500,98 L 50,98 Z'; + const expectedWidth = 500; + const expectedHeight = 100; + const expectedOffset = 50; + + expect(result.BezierPath).toBe(expectedBezierPath); + expect(result.BezierFill).toBe(expectedBezierFill); + expect(svgWidth).toBe(expectedWidth); + expect(svgHeight).toBe(expectedHeight); + expect(result.Offset).toBe(expectedOffset); + + // Verify line points are integer pixel values + result.LinePoints.forEach((p, _i) => { + expect(Number.isInteger(p.x)).toBe(true); + expect(Number.isInteger(p.y)).toBe(true); + }); + + // Expected line points from the current algorithm: + // idx 0: itemSize=5, itemY=95, lineX=50 + // idx 1: itemSize=45, itemY=55, lineX=100 + // idx 2: itemSize=25, itemY=75, lineX=150 + // ...and so on + }); + }); +}); diff --git a/frontend/src/components/ReadingHistoryGraph.tsx b/frontend/src/components/ReadingHistoryGraph.tsx new file mode 100644 index 0000000..fa91f50 --- /dev/null +++ b/frontend/src/components/ReadingHistoryGraph.tsx @@ -0,0 +1,210 @@ +import type { GraphDataPoint } from '../generated/model'; + +interface ReadingHistoryGraphProps { + data: GraphDataPoint[]; +} + +export interface SVGPoint { + x: number; + y: number; +} + +function getSVGBezierOpposedLine( + pointA: SVGPoint, + pointB: SVGPoint +): { Length: number; Angle: number } { + const lengthX = pointB.x - pointA.x; + const lengthY = pointB.y - pointA.y; + + return { + Length: Math.floor(Math.sqrt(lengthX * lengthX + lengthY * lengthY)), + Angle: Math.trunc(Math.atan2(lengthY, lengthX)), + }; +} + +function getBezierControlPoint( + currentPoint: SVGPoint, + prevPoint: SVGPoint | null, + nextPoint: SVGPoint | null, + isReverse: boolean +): SVGPoint { + let pPrev = prevPoint; + let pNext = nextPoint; + if (!pPrev) { + pPrev = currentPoint; + } + if (!pNext) { + pNext = currentPoint; + } + + const smoothingRatio = 0.2; + const directionModifier = isReverse ? Math.PI : 0; + + const opposingLine = getSVGBezierOpposedLine(pPrev, pNext); + const lineAngle = opposingLine.Angle + directionModifier; + const lineLength = opposingLine.Length * smoothingRatio; + + return { + x: Math.floor(currentPoint.x + Math.trunc(Math.cos(lineAngle) * lineLength)), + y: Math.floor(currentPoint.y + Math.trunc(Math.sin(lineAngle) * lineLength)), + }; +} + +function getSVGBezierPath(points: SVGPoint[]): string { + if (points.length === 0) { + return ''; + } + + let bezierSVGPath = ''; + + for (let index = 0; index < points.length; index++) { + const point = points[index]; + if (!point) { + continue; + } + + if (index === 0) { + bezierSVGPath += `M ${point.x},${point.y}`; + continue; + } + + const pointMinusOne = points[index - 1]; + if (!pointMinusOne) { + continue; + } + + const pointPlusOne = points[index + 1] ?? point; + const pointMinusTwo = index - 2 >= 0 ? (points[index - 2] ?? null) : null; + + const startControlPoint = getBezierControlPoint(pointMinusOne, pointMinusTwo, point, false); + const endControlPoint = getBezierControlPoint(point, pointMinusOne, pointPlusOne, true); + + bezierSVGPath += ` C${startControlPoint.x},${startControlPoint.y} ${endControlPoint.x},${endControlPoint.y} ${point.x},${point.y}`; + } + + return bezierSVGPath; +} + +export interface SVGGraphData { + LinePoints: SVGPoint[]; + BezierPath: string; + BezierFill: string; + Offset: number; +} + +export function getSVGGraphData( + inputData: GraphDataPoint[], + svgWidth: number, + svgHeight: number +): SVGGraphData { + let maxHeight = 0; + for (const item of inputData) { + if (item.minutes_read > maxHeight) { + maxHeight = item.minutes_read; + } + } + + const sizePercentage = 0.5; + const sizeRatio = maxHeight > 0 ? (svgHeight * sizePercentage) / maxHeight : 0; + const blockOffset = inputData.length > 0 ? Math.floor(svgWidth / inputData.length) : 0; + + const linePoints: SVGPoint[] = []; + + let maxBX = 0; + let maxBY = 0; + let minBX = 0; + + for (let idx = 0; idx < inputData.length; idx++) { + const item = inputData[idx]; + if (!item) { + continue; + } + + const itemSize = Math.floor(item.minutes_read * sizeRatio); + const itemY = svgHeight - itemSize; + const lineX = (idx + 1) * blockOffset; + + linePoints.push({ x: lineX, y: itemY }); + + if (lineX > maxBX) { + maxBX = lineX; + } + + if (lineX < minBX) { + minBX = lineX; + } + + if (itemY > maxBY) { + maxBY = itemY; + } + } + + return { + LinePoints: linePoints, + BezierPath: getSVGBezierPath(linePoints), + BezierFill: `L ${Math.floor(maxBX)},${Math.floor(maxBY)} L ${Math.floor(minBX + blockOffset)},${Math.floor(maxBY)} Z`, + Offset: blockOffset, + }; +} + +function formatDate(dateString: string): string { + const date = new Date(dateString); + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps) { + const svgWidth = 800; + const svgHeight = 70; + + if (!data || data.length < 2) { + return ( +
+

No data available

+
+ ); + } + + const { BezierPath, BezierFill } = getSVGGraphData(data, svgWidth, svgHeight); + + return ( +
+ + + + +
+ {data.map((point, i) => ( +
+
+ {formatDate(point.date)} + {point.minutes_read} minutes +
+
+ ))} +
+
+ ); +} diff --git a/frontend/src/components/Skeleton.tsx b/frontend/src/components/Skeleton.tsx new file mode 100644 index 0000000..72c25aa --- /dev/null +++ b/frontend/src/components/Skeleton.tsx @@ -0,0 +1,215 @@ +import { cn } from '../utils/cn'; + +interface SkeletonProps { + className?: string; + variant?: 'default' | 'text' | 'circular' | 'rectangular'; + width?: string | number; + height?: string | number; + animation?: 'pulse' | 'wave' | 'none'; +} + +export function Skeleton({ + className = '', + variant = 'default', + width, + height, + animation = 'pulse', +}: SkeletonProps) { + const baseClasses = 'bg-surface-strong'; + + const variantClasses = { + default: 'rounded', + text: 'h-4 rounded-md', + circular: 'rounded-full', + rectangular: 'rounded-none', + }; + + const animationClasses = { + pulse: 'animate-pulse', + wave: 'animate-wave', + none: '', + }; + + const style = { + width: width !== undefined ? (typeof width === 'number' ? `${width}px` : width) : undefined, + height: + height !== undefined ? (typeof height === 'number' ? `${height}px` : height) : undefined, + }; + + return ( +
+ ); +} + +interface SkeletonTextProps { + lines?: number; + className?: string; + lineClassName?: string; +} + +export function SkeletonText({ lines = 3, className = '', lineClassName = '' }: SkeletonTextProps) { + return ( +
+ {Array.from({ length: lines }).map((_, i) => ( + 1 ? 'w-3/4' : 'w-full')} + /> + ))} +
+ ); +} + +interface SkeletonAvatarProps { + size?: number | 'sm' | 'md' | 'lg'; + className?: string; +} + +export function SkeletonAvatar({ size = 'md', className = '' }: SkeletonAvatarProps) { + const sizeMap = { + sm: 32, + md: 40, + lg: 56, + }; + + const pixelSize = typeof size === 'number' ? size : sizeMap[size]; + + return ; +} + +interface SkeletonCardProps { + className?: string; + showAvatar?: boolean; + showTitle?: boolean; + showText?: boolean; + textLines?: number; +} + +export function SkeletonCard({ + className = '', + showAvatar = false, + showTitle = true, + showText = true, + textLines = 3, +}: SkeletonCardProps) { + return ( +
+ {showAvatar && ( +
+ +
+ + +
+
+ )} + {showTitle && } + {showText && } +
+ ); +} + +interface SkeletonTableProps { + rows?: number; + columns?: number; + className?: string; + showHeader?: boolean; +} + +export function SkeletonTable({ + rows = 5, + columns = 4, + className = '', + showHeader = true, +}: SkeletonTableProps) { + return ( +
+
+ {showHeader && ( + + + {Array.from({ length: columns }).map((_, i) => ( + + ))} + + + )} + + {Array.from({ length: rows }).map((_, rowIndex) => ( + + {Array.from({ length: columns }).map((_, colIndex) => ( + + ))} + + ))} + +
+ +
+ +
+ + ); +} + +interface SkeletonButtonProps { + className?: string; + width?: string | number; +} + +export function SkeletonButton({ className = '', width }: SkeletonButtonProps) { + return ( + + ); +} + +interface PageLoaderProps { + message?: string; + className?: string; +} + +export function PageLoader({ message = 'Loading...', className = '' }: PageLoaderProps) { + return ( +
+
+
+
+

{message}

+
+ ); +} + +interface InlineLoaderProps { + size?: 'sm' | 'md' | 'lg'; + className?: string; +} + +export function InlineLoader({ size = 'md', className = '' }: InlineLoaderProps) { + const sizeMap = { + sm: 'h-4 w-4 border-2', + md: 'h-6 w-6 border-[3px]', + lg: 'h-8 w-8 border-4', + }; + + return ( +
+
+
+ ); +} + +export { SkeletonTable as SkeletonTableExport }; diff --git a/frontend/src/components/Table.test.tsx b/frontend/src/components/Table.test.tsx new file mode 100644 index 0000000..7d1e08d --- /dev/null +++ b/frontend/src/components/Table.test.tsx @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { Table, type Column } from './Table'; + +interface TestRow { + id: string; + name: string; + role: string; +} + +const columns: Column[] = [ + { + key: 'name', + header: 'Name', + }, + { + key: 'role', + header: 'Role', + }, +]; + +const data: TestRow[] = [ + { id: 'user-1', name: 'Ada', role: 'Admin' }, + { id: 'user-2', name: 'Grace', role: 'Reader' }, +]; + +describe('Table', () => { + it('renders a skeleton table while loading', () => { + const { container } = render(); + + expect(screen.queryByText('No Results')).not.toBeInTheDocument(); + expect(container.querySelectorAll('tbody tr')).toHaveLength(5); + }); + + it('renders the empty state message when there is no data', () => { + render(
); + + expect(screen.getByText('Nothing here')).toBeInTheDocument(); + }); + + it('uses a custom render function for column output', () => { + const customColumns: Column[] = [ + { + key: 'name', + header: 'Name', + render: (_value, row, index) => `${index + 1}. ${row.name.toUpperCase()}`, + }, + ]; + + render(
); + + expect(screen.getByText('1. ADA')).toBeInTheDocument(); + expect(screen.getByText('2. GRACE')).toBeInTheDocument(); + }); + +}); diff --git a/frontend/src/components/Table.tsx b/frontend/src/components/Table.tsx new file mode 100644 index 0000000..eb2ca2f --- /dev/null +++ b/frontend/src/components/Table.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { Skeleton } from './Skeleton'; +import { cn } from '../utils/cn'; + +export interface Column { + key: keyof T; + header: string; + render?: (value: T[keyof T], _row: T, _index: number) => React.ReactNode; + className?: string; +} + +export interface TableProps { + columns: Column[]; + data: T[]; + loading?: boolean; + emptyMessage?: string; + rowKey?: keyof T | ((row: T) => string); +} + +function SkeletonTable({ + rows = 5, + columns = 4, + className = '', +}: { + rows?: number; + columns?: number; + className?: string; +}) { + return ( +
+
+ + + {Array.from({ length: columns }).map((_, i) => ( + + ))} + + + + {Array.from({ length: rows }).map((_, rowIndex) => ( + + {Array.from({ length: columns }).map((_, colIndex) => ( + + ))} + + ))} + +
+ +
+ +
+
+ ); +} + +export function Table({ + columns, + data, + loading = false, + emptyMessage = 'No Results', + rowKey, +}: TableProps) { + const getRowKey = (row: T, index: number): string => { + if (typeof rowKey === 'function') { + return rowKey(row); + } + if (rowKey) { + return String(row[rowKey] ?? index); + } + return `row-${index}`; + }; + + if (loading) { + return ; + } + + return ( +
+
+ + + + {columns.map(column => ( + + ))} + + + + {data.length === 0 ? ( + + + + ) : ( + data.map((row, index) => ( + + {columns.map(column => ( + + ))} + + )) + )} + +
+ {column.header} +
+ {emptyMessage} +
+ {column.render + ? column.render(row[column.key], row, index) + : (row[column.key] as React.ReactNode)} +
+
+
+ ); +} diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx new file mode 100644 index 0000000..60588a6 --- /dev/null +++ b/frontend/src/components/Toast.tsx @@ -0,0 +1,87 @@ +import { useEffect, useState } from 'react'; +import { InfoIcon, WarningIcon, ErrorIcon, CloseIcon } from '../icons'; + +export type ToastType = 'info' | 'warning' | 'error'; + +export interface ToastProps { + id: string; + type: ToastType; + message: string; + duration?: number; + onClose?: (id: string) => void; +} + +const getToastStyles = (_type: ToastType) => { + const baseStyles = + 'flex items-center gap-3 rounded-lg border-l-4 p-4 shadow-lg transition-all duration-300'; + + const typeStyles = { + info: 'border-secondary-500 bg-secondary-100', + warning: 'border-yellow-500 bg-yellow-100', + error: 'border-red-500 bg-red-100', + }; + + const iconStyles = { + info: 'text-secondary-700', + warning: 'text-yellow-700', + error: 'text-red-700', + }; + + const textStyles = { + info: 'text-secondary-900', + warning: 'text-yellow-900', + error: 'text-red-900', + }; + + return { baseStyles, typeStyles, iconStyles, textStyles }; +}; + +export function Toast({ id, type, message, duration = 5000, onClose }: ToastProps) { + const [isVisible, setIsVisible] = useState(true); + const [isAnimatingOut, setIsAnimatingOut] = useState(false); + + const { baseStyles, typeStyles, iconStyles, textStyles } = getToastStyles(type); + + const handleClose = () => { + setIsAnimatingOut(true); + setTimeout(() => { + setIsVisible(false); + onClose?.(id); + }, 300); + }; + + useEffect(() => { + if (duration > 0) { + const timer = setTimeout(handleClose, duration); + return () => clearTimeout(timer); + } + }, [duration]); + + if (!isVisible) { + return null; + } + + const icons = { + info: , + warning: , + error: , + }; + + return ( +
+ {icons[type]} +

{message}

+ +
+ ); +} diff --git a/frontend/src/components/ToastContext.tsx b/frontend/src/components/ToastContext.tsx new file mode 100644 index 0000000..0e12ec7 --- /dev/null +++ b/frontend/src/components/ToastContext.tsx @@ -0,0 +1,95 @@ +import { createContext, useContext, useState, useCallback, ReactNode } from 'react'; +import { Toast, ToastType, ToastProps } from './Toast'; + +interface ToastContextType { + showToast: (message: string, type?: ToastType, duration?: number) => string; + showInfo: (message: string, duration?: number) => string; + showWarning: (message: string, duration?: number) => string; + showError: (message: string, duration?: number) => string; + removeToast: (id: string) => void; + clearToasts: () => void; +} + +const ToastContext = createContext(undefined); + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState<(ToastProps & { id: string })[]>([]); + + const removeToast = useCallback((id: string) => { + setToasts(prev => prev.filter(toast => toast.id !== id)); + }, []); + + const showToast = useCallback( + (message: string, _type: ToastType = 'info', _duration?: number): string => { + const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + setToasts(prev => [ + ...prev, + { id, type: _type, message, duration: _duration, onClose: removeToast }, + ]); + return id; + }, + [removeToast] + ); + + const showInfo = useCallback( + (message: string, _duration?: number) => { + return showToast(message, 'info', _duration); + }, + [showToast] + ); + + const showWarning = useCallback( + (message: string, _duration?: number) => { + return showToast(message, 'warning', _duration); + }, + [showToast] + ); + + const showError = useCallback( + (message: string, _duration?: number) => { + return showToast(message, 'error', _duration); + }, + [showToast] + ); + + const clearToasts = useCallback(() => { + setToasts([]); + }, []); + + return ( + + {children} + + + ); +} + +interface ToastContainerProps { + toasts: (ToastProps & { id: string })[]; +} + +function ToastContainer({ toasts }: ToastContainerProps) { + if (toasts.length === 0) { + return null; + } + + return ( +
+ {toasts.map(toast => ( +
+ +
+ ))} +
+ ); +} + +export function useToasts() { + const context = useContext(ToastContext); + if (context === undefined) { + throw new Error('useToasts must be used within a ToastProvider'); + } + return context; +} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 0000000..cc324ff --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1,23 @@ +// Reading History Graph +export { default as ReadingHistoryGraph } from './ReadingHistoryGraph'; + +// Toast components +export { Toast } from './Toast'; +export { ToastProvider, useToasts } from './ToastContext'; +export type { ToastType, ToastProps } from './Toast'; + +// Skeleton components +export { + Skeleton, + SkeletonText, + SkeletonAvatar, + SkeletonCard, + SkeletonTable, + SkeletonButton, + PageLoader, + InlineLoader, +} from './Skeleton'; +export { LoadingState } from './LoadingState'; + +// Field components +export { Field, FieldLabel, FieldValue, FieldActions } from './Field'; diff --git a/frontend/src/generated/anthoLumeAPIV1.ts b/frontend/src/generated/anthoLumeAPIV1.ts new file mode 100644 index 0000000..bfad9be --- /dev/null +++ b/frontend/src/generated/anthoLumeAPIV1.ts @@ -0,0 +1,4117 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import { + useMutation, + useQuery +} from '@tanstack/react-query'; +import type { + DataTag, + DefinedInitialDataOptions, + DefinedUseQueryResult, + MutationFunction, + QueryClient, + QueryFunction, + QueryKey, + UndefinedInitialDataOptions, + UseMutationOptions, + UseMutationResult, + UseQueryOptions, + UseQueryResult +} from '@tanstack/react-query'; + +import type { + ActivityResponse, + CreateActivityRequest, + CreateActivityResponse, + CreateDocumentBody, + DirectoryListResponse, + DocumentResponse, + DocumentsResponse, + EditDocumentBody, + ErrorResponse, + GetActivityParams, + GetAdmin200, + GetDocumentsParams, + GetImportDirectoryParams, + GetLogsParams, + GetProgressListParams, + GetSearchParams, + GraphDataResponse, + HomeResponse, + ImportResultsResponse, + InfoResponse, + LoginRequest, + LoginResponse, + LogsResponse, + MessageResponse, + PostAdminActionBody, + PostImportBody, + PostSearchBody, + ProgressListResponse, + ProgressResponse, + SearchResponse, + SettingsResponse, + StreaksResponse, + UpdateProgressRequest, + UpdateProgressResponse, + UpdateSettingsRequest, + UpdateUserBody, + UploadDocumentCoverBody, + UserStatisticsResponse, + UsersResponse +} from './model'; + + + + + +/** + * @summary List documents + */ +export type getDocumentsResponse200 = { + data: DocumentsResponse + status: 200 +} + +export type getDocumentsResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getDocumentsResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getDocumentsResponseSuccess = (getDocumentsResponse200) & { + headers: Headers; +}; +export type getDocumentsResponseError = (getDocumentsResponse401 | getDocumentsResponse500) & { + headers: Headers; +}; + +export type getDocumentsResponse = (getDocumentsResponseSuccess | getDocumentsResponseError) + +export const getGetDocumentsUrl = (params?: GetDocumentsParams,) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()) + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 ? `/api/v1/documents?${stringifiedParams}` : `/api/v1/documents` +} + +export const getDocuments = async (params?: GetDocumentsParams, options?: RequestInit): Promise => { + + const res = await fetch(getGetDocumentsUrl(params), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getDocumentsResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getDocumentsResponse +} + + + + + +export const getGetDocumentsQueryKey = (params?: GetDocumentsParams,) => { + return [ + `/api/v1/documents`, ...(params ? [params] : []) + ] as const; + } + + +export const getGetDocumentsQueryOptions = >, TError = ErrorResponse>(params?: GetDocumentsParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetDocumentsQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getDocuments(params, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetDocumentsQueryResult = NonNullable>> +export type GetDocumentsQueryError = ErrorResponse + + +export function useGetDocuments>, TError = ErrorResponse>( + params: undefined | GetDocumentsParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetDocuments>, TError = ErrorResponse>( + params?: GetDocumentsParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetDocuments>, TError = ErrorResponse>( + params?: GetDocumentsParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary List documents + */ + +export function useGetDocuments>, TError = ErrorResponse>( + params?: GetDocumentsParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetDocumentsQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Upload a new document + */ +export type createDocumentResponse200 = { + data: DocumentResponse + status: 200 +} + +export type createDocumentResponse400 = { + data: ErrorResponse + status: 400 +} + +export type createDocumentResponse401 = { + data: ErrorResponse + status: 401 +} + +export type createDocumentResponse500 = { + data: ErrorResponse + status: 500 +} + +export type createDocumentResponseSuccess = (createDocumentResponse200) & { + headers: Headers; +}; +export type createDocumentResponseError = (createDocumentResponse400 | createDocumentResponse401 | createDocumentResponse500) & { + headers: Headers; +}; + +export type createDocumentResponse = (createDocumentResponseSuccess | createDocumentResponseError) + +export const getCreateDocumentUrl = () => { + + + + + return `/api/v1/documents` +} + +export const createDocument = async (createDocumentBody: CreateDocumentBody, options?: RequestInit): Promise => { + const formData = new FormData(); +formData.append(`document_file`, createDocumentBody.document_file); + + const res = await fetch(getCreateDocumentUrl(), + { + ...options, + method: 'POST' + , + body: + formData, + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: createDocumentResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as createDocumentResponse +} + + + + +export const getCreateDocumentMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: CreateDocumentBody}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: CreateDocumentBody}, TContext> => { + +const mutationKey = ['createDocument']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: CreateDocumentBody}> = (props) => { + const {data} = props ?? {}; + + return createDocument(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type CreateDocumentMutationResult = NonNullable>> + export type CreateDocumentMutationBody = CreateDocumentBody + export type CreateDocumentMutationError = ErrorResponse + + /** + * @summary Upload a new document + */ +export const useCreateDocument = (options?: { mutation?:UseMutationOptions>, TError,{data: CreateDocumentBody}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: CreateDocumentBody}, + TContext + > => { + return useMutation(getCreateDocumentMutationOptions(options), queryClient); + } + +/** + * @summary Get a single document + */ +export type getDocumentResponse200 = { + data: DocumentResponse + status: 200 +} + +export type getDocumentResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getDocumentResponse404 = { + data: ErrorResponse + status: 404 +} + +export type getDocumentResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getDocumentResponseSuccess = (getDocumentResponse200) & { + headers: Headers; +}; +export type getDocumentResponseError = (getDocumentResponse401 | getDocumentResponse404 | getDocumentResponse500) & { + headers: Headers; +}; + +export type getDocumentResponse = (getDocumentResponseSuccess | getDocumentResponseError) + +export const getGetDocumentUrl = (id: string,) => { + + + + + return `/api/v1/documents/${id}` +} + +export const getDocument = async (id: string, options?: RequestInit): Promise => { + + const res = await fetch(getGetDocumentUrl(id), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getDocumentResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getDocumentResponse +} + + + + + +export const getGetDocumentQueryKey = (id: string,) => { + return [ + `/api/v1/documents/${id}` + ] as const; + } + + +export const getGetDocumentQueryOptions = >, TError = ErrorResponse>(id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetDocumentQueryKey(id); + + + + const queryFn: QueryFunction>> = ({ signal }) => getDocument(id, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetDocumentQueryResult = NonNullable>> +export type GetDocumentQueryError = ErrorResponse + + +export function useGetDocument>, TError = ErrorResponse>( + id: string, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetDocument>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetDocument>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get a single document + */ + +export function useGetDocument>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetDocumentQueryOptions(id,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Update document editable fields + */ +export type editDocumentResponse200 = { + data: DocumentResponse + status: 200 +} + +export type editDocumentResponse400 = { + data: ErrorResponse + status: 400 +} + +export type editDocumentResponse401 = { + data: ErrorResponse + status: 401 +} + +export type editDocumentResponse404 = { + data: ErrorResponse + status: 404 +} + +export type editDocumentResponse500 = { + data: ErrorResponse + status: 500 +} + +export type editDocumentResponseSuccess = (editDocumentResponse200) & { + headers: Headers; +}; +export type editDocumentResponseError = (editDocumentResponse400 | editDocumentResponse401 | editDocumentResponse404 | editDocumentResponse500) & { + headers: Headers; +}; + +export type editDocumentResponse = (editDocumentResponseSuccess | editDocumentResponseError) + +export const getEditDocumentUrl = (id: string,) => { + + + + + return `/api/v1/documents/${id}` +} + +export const editDocument = async (id: string, + editDocumentBody: EditDocumentBody, options?: RequestInit): Promise => { + + const res = await fetch(getEditDocumentUrl(id), + { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify( + editDocumentBody,) + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: editDocumentResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as editDocumentResponse +} + + + + +export const getEditDocumentMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{id: string;data: EditDocumentBody}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{id: string;data: EditDocumentBody}, TContext> => { + +const mutationKey = ['editDocument']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {id: string;data: EditDocumentBody}> = (props) => { + const {id,data} = props ?? {}; + + return editDocument(id,data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type EditDocumentMutationResult = NonNullable>> + export type EditDocumentMutationBody = EditDocumentBody + export type EditDocumentMutationError = ErrorResponse + + /** + * @summary Update document editable fields + */ +export const useEditDocument = (options?: { mutation?:UseMutationOptions>, TError,{id: string;data: EditDocumentBody}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {id: string;data: EditDocumentBody}, + TContext + > => { + return useMutation(getEditDocumentMutationOptions(options), queryClient); + } + +/** + * @summary Get document cover image + */ +export type getDocumentCoverResponse200ImageJpeg = { + data: Blob + status: 200 +} + +export type getDocumentCoverResponse200ImagePng = { + data: Blob + status: 200 +} + +export type getDocumentCoverResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getDocumentCoverResponse404 = { + data: ErrorResponse + status: 404 +} + +export type getDocumentCoverResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getDocumentCoverResponseSuccess = (getDocumentCoverResponse200ImageJpeg | getDocumentCoverResponse200ImagePng) & { + headers: Headers; +}; +export type getDocumentCoverResponseError = (getDocumentCoverResponse401 | getDocumentCoverResponse404 | getDocumentCoverResponse500) & { + headers: Headers; +}; + +export type getDocumentCoverResponse = (getDocumentCoverResponseSuccess | getDocumentCoverResponseError) + +export const getGetDocumentCoverUrl = (id: string,) => { + + + + + return `/api/v1/documents/${id}/cover` +} + +export const getDocumentCover = async (id: string, options?: RequestInit): Promise => { + + const res = await fetch(getGetDocumentCoverUrl(id), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getDocumentCoverResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getDocumentCoverResponse +} + + + + + +export const getGetDocumentCoverQueryKey = (id: string,) => { + return [ + `/api/v1/documents/${id}/cover` + ] as const; + } + + +export const getGetDocumentCoverQueryOptions = >, TError = ErrorResponse>(id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetDocumentCoverQueryKey(id); + + + + const queryFn: QueryFunction>> = ({ signal }) => getDocumentCover(id, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetDocumentCoverQueryResult = NonNullable>> +export type GetDocumentCoverQueryError = ErrorResponse + + +export function useGetDocumentCover>, TError = ErrorResponse>( + id: string, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetDocumentCover>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetDocumentCover>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get document cover image + */ + +export function useGetDocumentCover>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetDocumentCoverQueryOptions(id,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Upload document cover image + */ +export type uploadDocumentCoverResponse200 = { + data: DocumentResponse + status: 200 +} + +export type uploadDocumentCoverResponse400 = { + data: ErrorResponse + status: 400 +} + +export type uploadDocumentCoverResponse401 = { + data: ErrorResponse + status: 401 +} + +export type uploadDocumentCoverResponse404 = { + data: ErrorResponse + status: 404 +} + +export type uploadDocumentCoverResponse500 = { + data: ErrorResponse + status: 500 +} + +export type uploadDocumentCoverResponseSuccess = (uploadDocumentCoverResponse200) & { + headers: Headers; +}; +export type uploadDocumentCoverResponseError = (uploadDocumentCoverResponse400 | uploadDocumentCoverResponse401 | uploadDocumentCoverResponse404 | uploadDocumentCoverResponse500) & { + headers: Headers; +}; + +export type uploadDocumentCoverResponse = (uploadDocumentCoverResponseSuccess | uploadDocumentCoverResponseError) + +export const getUploadDocumentCoverUrl = (id: string,) => { + + + + + return `/api/v1/documents/${id}/cover` +} + +export const uploadDocumentCover = async (id: string, + uploadDocumentCoverBody: UploadDocumentCoverBody, options?: RequestInit): Promise => { + const formData = new FormData(); +formData.append(`cover_file`, uploadDocumentCoverBody.cover_file); + + const res = await fetch(getUploadDocumentCoverUrl(id), + { + ...options, + method: 'POST' + , + body: + formData, + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: uploadDocumentCoverResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as uploadDocumentCoverResponse +} + + + + +export const getUploadDocumentCoverMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{id: string;data: UploadDocumentCoverBody}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{id: string;data: UploadDocumentCoverBody}, TContext> => { + +const mutationKey = ['uploadDocumentCover']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {id: string;data: UploadDocumentCoverBody}> = (props) => { + const {id,data} = props ?? {}; + + return uploadDocumentCover(id,data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type UploadDocumentCoverMutationResult = NonNullable>> + export type UploadDocumentCoverMutationBody = UploadDocumentCoverBody + export type UploadDocumentCoverMutationError = ErrorResponse + + /** + * @summary Upload document cover image + */ +export const useUploadDocumentCover = (options?: { mutation?:UseMutationOptions>, TError,{id: string;data: UploadDocumentCoverBody}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {id: string;data: UploadDocumentCoverBody}, + TContext + > => { + return useMutation(getUploadDocumentCoverMutationOptions(options), queryClient); + } + +/** + * @summary Download document file + */ +export type getDocumentFileResponse200 = { + data: Blob + status: 200 +} + +export type getDocumentFileResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getDocumentFileResponse404 = { + data: ErrorResponse + status: 404 +} + +export type getDocumentFileResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getDocumentFileResponseSuccess = (getDocumentFileResponse200) & { + headers: Headers; +}; +export type getDocumentFileResponseError = (getDocumentFileResponse401 | getDocumentFileResponse404 | getDocumentFileResponse500) & { + headers: Headers; +}; + +export type getDocumentFileResponse = (getDocumentFileResponseSuccess | getDocumentFileResponseError) + +export const getGetDocumentFileUrl = (id: string,) => { + + + + + return `/api/v1/documents/${id}/file` +} + +export const getDocumentFile = async (id: string, options?: RequestInit): Promise => { + + const res = await fetch(getGetDocumentFileUrl(id), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getDocumentFileResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getDocumentFileResponse +} + + + + + +export const getGetDocumentFileQueryKey = (id: string,) => { + return [ + `/api/v1/documents/${id}/file` + ] as const; + } + + +export const getGetDocumentFileQueryOptions = >, TError = ErrorResponse>(id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetDocumentFileQueryKey(id); + + + + const queryFn: QueryFunction>> = ({ signal }) => getDocumentFile(id, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetDocumentFileQueryResult = NonNullable>> +export type GetDocumentFileQueryError = ErrorResponse + + +export function useGetDocumentFile>, TError = ErrorResponse>( + id: string, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetDocumentFile>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetDocumentFile>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Download document file + */ + +export function useGetDocumentFile>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetDocumentFileQueryOptions(id,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary List progress records + */ +export type getProgressListResponse200 = { + data: ProgressListResponse + status: 200 +} + +export type getProgressListResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getProgressListResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getProgressListResponseSuccess = (getProgressListResponse200) & { + headers: Headers; +}; +export type getProgressListResponseError = (getProgressListResponse401 | getProgressListResponse500) & { + headers: Headers; +}; + +export type getProgressListResponse = (getProgressListResponseSuccess | getProgressListResponseError) + +export const getGetProgressListUrl = (params?: GetProgressListParams,) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()) + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 ? `/api/v1/progress?${stringifiedParams}` : `/api/v1/progress` +} + +export const getProgressList = async (params?: GetProgressListParams, options?: RequestInit): Promise => { + + const res = await fetch(getGetProgressListUrl(params), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getProgressListResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getProgressListResponse +} + + + + + +export const getGetProgressListQueryKey = (params?: GetProgressListParams,) => { + return [ + `/api/v1/progress`, ...(params ? [params] : []) + ] as const; + } + + +export const getGetProgressListQueryOptions = >, TError = ErrorResponse>(params?: GetProgressListParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetProgressListQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getProgressList(params, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetProgressListQueryResult = NonNullable>> +export type GetProgressListQueryError = ErrorResponse + + +export function useGetProgressList>, TError = ErrorResponse>( + params: undefined | GetProgressListParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetProgressList>, TError = ErrorResponse>( + params?: GetProgressListParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetProgressList>, TError = ErrorResponse>( + params?: GetProgressListParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary List progress records + */ + +export function useGetProgressList>, TError = ErrorResponse>( + params?: GetProgressListParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetProgressListQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Update document progress + */ +export type updateProgressResponse200 = { + data: UpdateProgressResponse + status: 200 +} + +export type updateProgressResponse400 = { + data: ErrorResponse + status: 400 +} + +export type updateProgressResponse401 = { + data: ErrorResponse + status: 401 +} + +export type updateProgressResponse500 = { + data: ErrorResponse + status: 500 +} + +export type updateProgressResponseSuccess = (updateProgressResponse200) & { + headers: Headers; +}; +export type updateProgressResponseError = (updateProgressResponse400 | updateProgressResponse401 | updateProgressResponse500) & { + headers: Headers; +}; + +export type updateProgressResponse = (updateProgressResponseSuccess | updateProgressResponseError) + +export const getUpdateProgressUrl = () => { + + + + + return `/api/v1/progress` +} + +export const updateProgress = async (updateProgressRequest: UpdateProgressRequest, options?: RequestInit): Promise => { + + const res = await fetch(getUpdateProgressUrl(), + { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify( + updateProgressRequest,) + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: updateProgressResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as updateProgressResponse +} + + + + +export const getUpdateProgressMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: UpdateProgressRequest}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: UpdateProgressRequest}, TContext> => { + +const mutationKey = ['updateProgress']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: UpdateProgressRequest}> = (props) => { + const {data} = props ?? {}; + + return updateProgress(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type UpdateProgressMutationResult = NonNullable>> + export type UpdateProgressMutationBody = UpdateProgressRequest + export type UpdateProgressMutationError = ErrorResponse + + /** + * @summary Update document progress + */ +export const useUpdateProgress = (options?: { mutation?:UseMutationOptions>, TError,{data: UpdateProgressRequest}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: UpdateProgressRequest}, + TContext + > => { + return useMutation(getUpdateProgressMutationOptions(options), queryClient); + } + +/** + * @summary Get document progress + */ +export type getProgressResponse200 = { + data: ProgressResponse + status: 200 +} + +export type getProgressResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getProgressResponse404 = { + data: ErrorResponse + status: 404 +} + +export type getProgressResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getProgressResponseSuccess = (getProgressResponse200) & { + headers: Headers; +}; +export type getProgressResponseError = (getProgressResponse401 | getProgressResponse404 | getProgressResponse500) & { + headers: Headers; +}; + +export type getProgressResponse = (getProgressResponseSuccess | getProgressResponseError) + +export const getGetProgressUrl = (id: string,) => { + + + + + return `/api/v1/progress/${id}` +} + +export const getProgress = async (id: string, options?: RequestInit): Promise => { + + const res = await fetch(getGetProgressUrl(id), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getProgressResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getProgressResponse +} + + + + + +export const getGetProgressQueryKey = (id: string,) => { + return [ + `/api/v1/progress/${id}` + ] as const; + } + + +export const getGetProgressQueryOptions = >, TError = ErrorResponse>(id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetProgressQueryKey(id); + + + + const queryFn: QueryFunction>> = ({ signal }) => getProgress(id, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, enabled: !!(id), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetProgressQueryResult = NonNullable>> +export type GetProgressQueryError = ErrorResponse + + +export function useGetProgress>, TError = ErrorResponse>( + id: string, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetProgress>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetProgress>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get document progress + */ + +export function useGetProgress>, TError = ErrorResponse>( + id: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetProgressQueryOptions(id,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Get activity data + */ +export type getActivityResponse200 = { + data: ActivityResponse + status: 200 +} + +export type getActivityResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getActivityResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getActivityResponseSuccess = (getActivityResponse200) & { + headers: Headers; +}; +export type getActivityResponseError = (getActivityResponse401 | getActivityResponse500) & { + headers: Headers; +}; + +export type getActivityResponse = (getActivityResponseSuccess | getActivityResponseError) + +export const getGetActivityUrl = (params?: GetActivityParams,) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()) + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 ? `/api/v1/activity?${stringifiedParams}` : `/api/v1/activity` +} + +export const getActivity = async (params?: GetActivityParams, options?: RequestInit): Promise => { + + const res = await fetch(getGetActivityUrl(params), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getActivityResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getActivityResponse +} + + + + + +export const getGetActivityQueryKey = (params?: GetActivityParams,) => { + return [ + `/api/v1/activity`, ...(params ? [params] : []) + ] as const; + } + + +export const getGetActivityQueryOptions = >, TError = ErrorResponse>(params?: GetActivityParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetActivityQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getActivity(params, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetActivityQueryResult = NonNullable>> +export type GetActivityQueryError = ErrorResponse + + +export function useGetActivity>, TError = ErrorResponse>( + params: undefined | GetActivityParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetActivity>, TError = ErrorResponse>( + params?: GetActivityParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetActivity>, TError = ErrorResponse>( + params?: GetActivityParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get activity data + */ + +export function useGetActivity>, TError = ErrorResponse>( + params?: GetActivityParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetActivityQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Create activity records + */ +export type createActivityResponse200 = { + data: CreateActivityResponse + status: 200 +} + +export type createActivityResponse400 = { + data: ErrorResponse + status: 400 +} + +export type createActivityResponse401 = { + data: ErrorResponse + status: 401 +} + +export type createActivityResponse500 = { + data: ErrorResponse + status: 500 +} + +export type createActivityResponseSuccess = (createActivityResponse200) & { + headers: Headers; +}; +export type createActivityResponseError = (createActivityResponse400 | createActivityResponse401 | createActivityResponse500) & { + headers: Headers; +}; + +export type createActivityResponse = (createActivityResponseSuccess | createActivityResponseError) + +export const getCreateActivityUrl = () => { + + + + + return `/api/v1/activity` +} + +export const createActivity = async (createActivityRequest: CreateActivityRequest, options?: RequestInit): Promise => { + + const res = await fetch(getCreateActivityUrl(), + { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify( + createActivityRequest,) + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: createActivityResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as createActivityResponse +} + + + + +export const getCreateActivityMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: CreateActivityRequest}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: CreateActivityRequest}, TContext> => { + +const mutationKey = ['createActivity']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: CreateActivityRequest}> = (props) => { + const {data} = props ?? {}; + + return createActivity(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type CreateActivityMutationResult = NonNullable>> + export type CreateActivityMutationBody = CreateActivityRequest + export type CreateActivityMutationError = ErrorResponse + + /** + * @summary Create activity records + */ +export const useCreateActivity = (options?: { mutation?:UseMutationOptions>, TError,{data: CreateActivityRequest}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: CreateActivityRequest}, + TContext + > => { + return useMutation(getCreateActivityMutationOptions(options), queryClient); + } + +/** + * @summary Get user settings + */ +export type getSettingsResponse200 = { + data: SettingsResponse + status: 200 +} + +export type getSettingsResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getSettingsResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getSettingsResponseSuccess = (getSettingsResponse200) & { + headers: Headers; +}; +export type getSettingsResponseError = (getSettingsResponse401 | getSettingsResponse500) & { + headers: Headers; +}; + +export type getSettingsResponse = (getSettingsResponseSuccess | getSettingsResponseError) + +export const getGetSettingsUrl = () => { + + + + + return `/api/v1/settings` +} + +export const getSettings = async ( options?: RequestInit): Promise => { + + const res = await fetch(getGetSettingsUrl(), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getSettingsResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getSettingsResponse +} + + + + + +export const getGetSettingsQueryKey = () => { + return [ + `/api/v1/settings` + ] as const; + } + + +export const getGetSettingsQueryOptions = >, TError = ErrorResponse>( options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetSettingsQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getSettings({ signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetSettingsQueryResult = NonNullable>> +export type GetSettingsQueryError = ErrorResponse + + +export function useGetSettings>, TError = ErrorResponse>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetSettings>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetSettings>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get user settings + */ + +export function useGetSettings>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetSettingsQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Update user settings + */ +export type updateSettingsResponse200 = { + data: SettingsResponse + status: 200 +} + +export type updateSettingsResponse400 = { + data: ErrorResponse + status: 400 +} + +export type updateSettingsResponse401 = { + data: ErrorResponse + status: 401 +} + +export type updateSettingsResponse500 = { + data: ErrorResponse + status: 500 +} + +export type updateSettingsResponseSuccess = (updateSettingsResponse200) & { + headers: Headers; +}; +export type updateSettingsResponseError = (updateSettingsResponse400 | updateSettingsResponse401 | updateSettingsResponse500) & { + headers: Headers; +}; + +export type updateSettingsResponse = (updateSettingsResponseSuccess | updateSettingsResponseError) + +export const getUpdateSettingsUrl = () => { + + + + + return `/api/v1/settings` +} + +export const updateSettings = async (updateSettingsRequest: UpdateSettingsRequest, options?: RequestInit): Promise => { + + const res = await fetch(getUpdateSettingsUrl(), + { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify( + updateSettingsRequest,) + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: updateSettingsResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as updateSettingsResponse +} + + + + +export const getUpdateSettingsMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: UpdateSettingsRequest}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: UpdateSettingsRequest}, TContext> => { + +const mutationKey = ['updateSettings']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: UpdateSettingsRequest}> = (props) => { + const {data} = props ?? {}; + + return updateSettings(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type UpdateSettingsMutationResult = NonNullable>> + export type UpdateSettingsMutationBody = UpdateSettingsRequest + export type UpdateSettingsMutationError = ErrorResponse + + /** + * @summary Update user settings + */ +export const useUpdateSettings = (options?: { mutation?:UseMutationOptions>, TError,{data: UpdateSettingsRequest}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: UpdateSettingsRequest}, + TContext + > => { + return useMutation(getUpdateSettingsMutationOptions(options), queryClient); + } + +/** + * @summary User login + */ +export type loginResponse200 = { + data: LoginResponse + status: 200 +} + +export type loginResponse400 = { + data: ErrorResponse + status: 400 +} + +export type loginResponse401 = { + data: ErrorResponse + status: 401 +} + +export type loginResponse500 = { + data: ErrorResponse + status: 500 +} + +export type loginResponseSuccess = (loginResponse200) & { + headers: Headers; +}; +export type loginResponseError = (loginResponse400 | loginResponse401 | loginResponse500) & { + headers: Headers; +}; + +export type loginResponse = (loginResponseSuccess | loginResponseError) + +export const getLoginUrl = () => { + + + + + return `/api/v1/auth/login` +} + +export const login = async (loginRequest: LoginRequest, options?: RequestInit): Promise => { + + const res = await fetch(getLoginUrl(), + { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify( + loginRequest,) + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: loginResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as loginResponse +} + + + + +export const getLoginMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: LoginRequest}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: LoginRequest}, TContext> => { + +const mutationKey = ['login']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: LoginRequest}> = (props) => { + const {data} = props ?? {}; + + return login(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type LoginMutationResult = NonNullable>> + export type LoginMutationBody = LoginRequest + export type LoginMutationError = ErrorResponse + + /** + * @summary User login + */ +export const useLogin = (options?: { mutation?:UseMutationOptions>, TError,{data: LoginRequest}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: LoginRequest}, + TContext + > => { + return useMutation(getLoginMutationOptions(options), queryClient); + } + +/** + * @summary User registration + */ +export type registerResponse201 = { + data: LoginResponse + status: 201 +} + +export type registerResponse400 = { + data: ErrorResponse + status: 400 +} + +export type registerResponse403 = { + data: ErrorResponse + status: 403 +} + +export type registerResponse500 = { + data: ErrorResponse + status: 500 +} + +export type registerResponseSuccess = (registerResponse201) & { + headers: Headers; +}; +export type registerResponseError = (registerResponse400 | registerResponse403 | registerResponse500) & { + headers: Headers; +}; + +export type registerResponse = (registerResponseSuccess | registerResponseError) + +export const getRegisterUrl = () => { + + + + + return `/api/v1/auth/register` +} + +export const register = async (loginRequest: LoginRequest, options?: RequestInit): Promise => { + + const res = await fetch(getRegisterUrl(), + { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify( + loginRequest,) + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: registerResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as registerResponse +} + + + + +export const getRegisterMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: LoginRequest}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: LoginRequest}, TContext> => { + +const mutationKey = ['register']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: LoginRequest}> = (props) => { + const {data} = props ?? {}; + + return register(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type RegisterMutationResult = NonNullable>> + export type RegisterMutationBody = LoginRequest + export type RegisterMutationError = ErrorResponse + + /** + * @summary User registration + */ +export const useRegister = (options?: { mutation?:UseMutationOptions>, TError,{data: LoginRequest}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: LoginRequest}, + TContext + > => { + return useMutation(getRegisterMutationOptions(options), queryClient); + } + +/** + * @summary User logout + */ +export type logoutResponse200 = { + data: void + status: 200 +} + +export type logoutResponse401 = { + data: ErrorResponse + status: 401 +} + +export type logoutResponseSuccess = (logoutResponse200) & { + headers: Headers; +}; +export type logoutResponseError = (logoutResponse401) & { + headers: Headers; +}; + +export type logoutResponse = (logoutResponseSuccess | logoutResponseError) + +export const getLogoutUrl = () => { + + + + + return `/api/v1/auth/logout` +} + +export const logout = async ( options?: RequestInit): Promise => { + + const res = await fetch(getLogoutUrl(), + { + ...options, + method: 'POST' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: logoutResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as logoutResponse +} + + + + +export const getLogoutMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,void, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,void, TContext> => { + +const mutationKey = ['logout']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, void> = () => { + + + return logout(fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type LogoutMutationResult = NonNullable>> + + export type LogoutMutationError = ErrorResponse + + /** + * @summary User logout + */ +export const useLogout = (options?: { mutation?:UseMutationOptions>, TError,void, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + void, + TContext + > => { + return useMutation(getLogoutMutationOptions(options), queryClient); + } + +/** + * @summary Get current user info + */ +export type getMeResponse200 = { + data: LoginResponse + status: 200 +} + +export type getMeResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getMeResponseSuccess = (getMeResponse200) & { + headers: Headers; +}; +export type getMeResponseError = (getMeResponse401) & { + headers: Headers; +}; + +export type getMeResponse = (getMeResponseSuccess | getMeResponseError) + +export const getGetMeUrl = () => { + + + + + return `/api/v1/auth/me` +} + +export const getMe = async ( options?: RequestInit): Promise => { + + const res = await fetch(getGetMeUrl(), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getMeResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getMeResponse +} + + + + + +export const getGetMeQueryKey = () => { + return [ + `/api/v1/auth/me` + ] as const; + } + + +export const getGetMeQueryOptions = >, TError = ErrorResponse>( options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetMeQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getMe({ signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetMeQueryResult = NonNullable>> +export type GetMeQueryError = ErrorResponse + + +export function useGetMe>, TError = ErrorResponse>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetMe>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetMe>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get current user info + */ + +export function useGetMe>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetMeQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Get server information + */ +export type getInfoResponse200 = { + data: InfoResponse + status: 200 +} + +export type getInfoResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getInfoResponseSuccess = (getInfoResponse200) & { + headers: Headers; +}; +export type getInfoResponseError = (getInfoResponse500) & { + headers: Headers; +}; + +export type getInfoResponse = (getInfoResponseSuccess | getInfoResponseError) + +export const getGetInfoUrl = () => { + + + + + return `/api/v1/info` +} + +export const getInfo = async ( options?: RequestInit): Promise => { + + const res = await fetch(getGetInfoUrl(), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getInfoResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getInfoResponse +} + + + + + +export const getGetInfoQueryKey = () => { + return [ + `/api/v1/info` + ] as const; + } + + +export const getGetInfoQueryOptions = >, TError = ErrorResponse>( options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetInfoQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getInfo({ signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetInfoQueryResult = NonNullable>> +export type GetInfoQueryError = ErrorResponse + + +export function useGetInfo>, TError = ErrorResponse>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetInfo>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetInfo>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get server information + */ + +export function useGetInfo>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetInfoQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Get home page data + */ +export type getHomeResponse200 = { + data: HomeResponse + status: 200 +} + +export type getHomeResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getHomeResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getHomeResponseSuccess = (getHomeResponse200) & { + headers: Headers; +}; +export type getHomeResponseError = (getHomeResponse401 | getHomeResponse500) & { + headers: Headers; +}; + +export type getHomeResponse = (getHomeResponseSuccess | getHomeResponseError) + +export const getGetHomeUrl = () => { + + + + + return `/api/v1/home` +} + +export const getHome = async ( options?: RequestInit): Promise => { + + const res = await fetch(getGetHomeUrl(), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getHomeResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getHomeResponse +} + + + + + +export const getGetHomeQueryKey = () => { + return [ + `/api/v1/home` + ] as const; + } + + +export const getGetHomeQueryOptions = >, TError = ErrorResponse>( options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetHomeQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getHome({ signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetHomeQueryResult = NonNullable>> +export type GetHomeQueryError = ErrorResponse + + +export function useGetHome>, TError = ErrorResponse>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetHome>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetHome>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get home page data + */ + +export function useGetHome>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetHomeQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Get user streaks + */ +export type getStreaksResponse200 = { + data: StreaksResponse + status: 200 +} + +export type getStreaksResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getStreaksResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getStreaksResponseSuccess = (getStreaksResponse200) & { + headers: Headers; +}; +export type getStreaksResponseError = (getStreaksResponse401 | getStreaksResponse500) & { + headers: Headers; +}; + +export type getStreaksResponse = (getStreaksResponseSuccess | getStreaksResponseError) + +export const getGetStreaksUrl = () => { + + + + + return `/api/v1/home/streaks` +} + +export const getStreaks = async ( options?: RequestInit): Promise => { + + const res = await fetch(getGetStreaksUrl(), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getStreaksResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getStreaksResponse +} + + + + + +export const getGetStreaksQueryKey = () => { + return [ + `/api/v1/home/streaks` + ] as const; + } + + +export const getGetStreaksQueryOptions = >, TError = ErrorResponse>( options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetStreaksQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getStreaks({ signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetStreaksQueryResult = NonNullable>> +export type GetStreaksQueryError = ErrorResponse + + +export function useGetStreaks>, TError = ErrorResponse>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetStreaks>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetStreaks>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get user streaks + */ + +export function useGetStreaks>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetStreaksQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Get daily read stats graph data + */ +export type getGraphDataResponse200 = { + data: GraphDataResponse + status: 200 +} + +export type getGraphDataResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getGraphDataResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getGraphDataResponseSuccess = (getGraphDataResponse200) & { + headers: Headers; +}; +export type getGraphDataResponseError = (getGraphDataResponse401 | getGraphDataResponse500) & { + headers: Headers; +}; + +export type getGraphDataResponse = (getGraphDataResponseSuccess | getGraphDataResponseError) + +export const getGetGraphDataUrl = () => { + + + + + return `/api/v1/home/graph` +} + +export const getGraphData = async ( options?: RequestInit): Promise => { + + const res = await fetch(getGetGraphDataUrl(), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getGraphDataResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getGraphDataResponse +} + + + + + +export const getGetGraphDataQueryKey = () => { + return [ + `/api/v1/home/graph` + ] as const; + } + + +export const getGetGraphDataQueryOptions = >, TError = ErrorResponse>( options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetGraphDataQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getGraphData({ signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetGraphDataQueryResult = NonNullable>> +export type GetGraphDataQueryError = ErrorResponse + + +export function useGetGraphData>, TError = ErrorResponse>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetGraphData>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetGraphData>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get daily read stats graph data + */ + +export function useGetGraphData>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetGraphDataQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Get user statistics (leaderboards) + */ +export type getUserStatisticsResponse200 = { + data: UserStatisticsResponse + status: 200 +} + +export type getUserStatisticsResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getUserStatisticsResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getUserStatisticsResponseSuccess = (getUserStatisticsResponse200) & { + headers: Headers; +}; +export type getUserStatisticsResponseError = (getUserStatisticsResponse401 | getUserStatisticsResponse500) & { + headers: Headers; +}; + +export type getUserStatisticsResponse = (getUserStatisticsResponseSuccess | getUserStatisticsResponseError) + +export const getGetUserStatisticsUrl = () => { + + + + + return `/api/v1/home/statistics` +} + +export const getUserStatistics = async ( options?: RequestInit): Promise => { + + const res = await fetch(getGetUserStatisticsUrl(), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getUserStatisticsResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getUserStatisticsResponse +} + + + + + +export const getGetUserStatisticsQueryKey = () => { + return [ + `/api/v1/home/statistics` + ] as const; + } + + +export const getGetUserStatisticsQueryOptions = >, TError = ErrorResponse>( options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetUserStatisticsQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getUserStatistics({ signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetUserStatisticsQueryResult = NonNullable>> +export type GetUserStatisticsQueryError = ErrorResponse + + +export function useGetUserStatistics>, TError = ErrorResponse>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetUserStatistics>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetUserStatistics>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get user statistics (leaderboards) + */ + +export function useGetUserStatistics>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetUserStatisticsQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Search external book sources + */ +export type getSearchResponse200 = { + data: SearchResponse + status: 200 +} + +export type getSearchResponse400 = { + data: ErrorResponse + status: 400 +} + +export type getSearchResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getSearchResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getSearchResponseSuccess = (getSearchResponse200) & { + headers: Headers; +}; +export type getSearchResponseError = (getSearchResponse400 | getSearchResponse401 | getSearchResponse500) & { + headers: Headers; +}; + +export type getSearchResponse = (getSearchResponseSuccess | getSearchResponseError) + +export const getGetSearchUrl = (params: GetSearchParams,) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()) + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 ? `/api/v1/search?${stringifiedParams}` : `/api/v1/search` +} + +export const getSearch = async (params: GetSearchParams, options?: RequestInit): Promise => { + + const res = await fetch(getGetSearchUrl(params), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getSearchResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getSearchResponse +} + + + + + +export const getGetSearchQueryKey = (params?: GetSearchParams,) => { + return [ + `/api/v1/search`, ...(params ? [params] : []) + ] as const; + } + + +export const getGetSearchQueryOptions = >, TError = ErrorResponse>(params: GetSearchParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetSearchQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getSearch(params, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetSearchQueryResult = NonNullable>> +export type GetSearchQueryError = ErrorResponse + + +export function useGetSearch>, TError = ErrorResponse>( + params: GetSearchParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetSearch>, TError = ErrorResponse>( + params: GetSearchParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetSearch>, TError = ErrorResponse>( + params: GetSearchParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Search external book sources + */ + +export function useGetSearch>, TError = ErrorResponse>( + params: GetSearchParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetSearchQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Download search result + */ +export type postSearchResponse200 = { + data: void + status: 200 +} + +export type postSearchResponse401 = { + data: ErrorResponse + status: 401 +} + +export type postSearchResponse500 = { + data: ErrorResponse + status: 500 +} + +export type postSearchResponseSuccess = (postSearchResponse200) & { + headers: Headers; +}; +export type postSearchResponseError = (postSearchResponse401 | postSearchResponse500) & { + headers: Headers; +}; + +export type postSearchResponse = (postSearchResponseSuccess | postSearchResponseError) + +export const getPostSearchUrl = () => { + + + + + return `/api/v1/search` +} + +export const postSearch = async (postSearchBody: PostSearchBody, options?: RequestInit): Promise => { + const formUrlEncoded = new URLSearchParams(); +formUrlEncoded.append(`source`, postSearchBody.source); +formUrlEncoded.append(`title`, postSearchBody.title); +formUrlEncoded.append(`author`, postSearchBody.author); +formUrlEncoded.append(`id`, postSearchBody.id); + + const res = await fetch(getPostSearchUrl(), + { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...options?.headers }, + body: + formUrlEncoded, + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: postSearchResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as postSearchResponse +} + + + + +export const getPostSearchMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: PostSearchBody}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: PostSearchBody}, TContext> => { + +const mutationKey = ['postSearch']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: PostSearchBody}> = (props) => { + const {data} = props ?? {}; + + return postSearch(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type PostSearchMutationResult = NonNullable>> + export type PostSearchMutationBody = PostSearchBody + export type PostSearchMutationError = ErrorResponse + + /** + * @summary Download search result + */ +export const usePostSearch = (options?: { mutation?:UseMutationOptions>, TError,{data: PostSearchBody}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: PostSearchBody}, + TContext + > => { + return useMutation(getPostSearchMutationOptions(options), queryClient); + } + +/** + * @summary Get admin page data + */ +export type getAdminResponse200 = { + data: GetAdmin200 + status: 200 +} + +export type getAdminResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getAdminResponseSuccess = (getAdminResponse200) & { + headers: Headers; +}; +export type getAdminResponseError = (getAdminResponse401) & { + headers: Headers; +}; + +export type getAdminResponse = (getAdminResponseSuccess | getAdminResponseError) + +export const getGetAdminUrl = () => { + + + + + return `/api/v1/admin` +} + +export const getAdmin = async ( options?: RequestInit): Promise => { + + const res = await fetch(getGetAdminUrl(), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getAdminResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getAdminResponse +} + + + + + +export const getGetAdminQueryKey = () => { + return [ + `/api/v1/admin` + ] as const; + } + + +export const getGetAdminQueryOptions = >, TError = ErrorResponse>( options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetAdminQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getAdmin({ signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetAdminQueryResult = NonNullable>> +export type GetAdminQueryError = ErrorResponse + + +export function useGetAdmin>, TError = ErrorResponse>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetAdmin>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetAdmin>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get admin page data + */ + +export function useGetAdmin>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetAdminQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Perform admin action (backup, restore, etc.) + */ +export type postAdminActionResponse200ApplicationJson = { + data: MessageResponse + status: 200 +} + +export type postAdminActionResponse200ApplicationOctetStream = { + data: Blob + status: 200 +} + +export type postAdminActionResponse400 = { + data: ErrorResponse + status: 400 +} + +export type postAdminActionResponse401 = { + data: ErrorResponse + status: 401 +} + +export type postAdminActionResponse500 = { + data: ErrorResponse + status: 500 +} + +export type postAdminActionResponseSuccess = (postAdminActionResponse200ApplicationJson | postAdminActionResponse200ApplicationOctetStream) & { + headers: Headers; +}; +export type postAdminActionResponseError = (postAdminActionResponse400 | postAdminActionResponse401 | postAdminActionResponse500) & { + headers: Headers; +}; + +export type postAdminActionResponse = (postAdminActionResponseSuccess | postAdminActionResponseError) + +export const getPostAdminActionUrl = () => { + + + + + return `/api/v1/admin` +} + +export const postAdminAction = async (postAdminActionBody: PostAdminActionBody, options?: RequestInit): Promise => { + const formData = new FormData(); +formData.append(`action`, postAdminActionBody.action); +if(postAdminActionBody.backup_types !== undefined) { + postAdminActionBody.backup_types.forEach(value => formData.append(`backup_types`, value)); + } +if(postAdminActionBody.restore_file !== undefined) { + formData.append(`restore_file`, postAdminActionBody.restore_file); + } + + const res = await fetch(getPostAdminActionUrl(), + { + ...options, + method: 'POST' + , + body: + formData, + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: postAdminActionResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as postAdminActionResponse +} + + + + +export const getPostAdminActionMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: PostAdminActionBody}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: PostAdminActionBody}, TContext> => { + +const mutationKey = ['postAdminAction']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: PostAdminActionBody}> = (props) => { + const {data} = props ?? {}; + + return postAdminAction(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type PostAdminActionMutationResult = NonNullable>> + export type PostAdminActionMutationBody = PostAdminActionBody + export type PostAdminActionMutationError = ErrorResponse + + /** + * @summary Perform admin action (backup, restore, etc.) + */ +export const usePostAdminAction = (options?: { mutation?:UseMutationOptions>, TError,{data: PostAdminActionBody}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: PostAdminActionBody}, + TContext + > => { + return useMutation(getPostAdminActionMutationOptions(options), queryClient); + } + +/** + * @summary Get all users + */ +export type getUsersResponse200 = { + data: UsersResponse + status: 200 +} + +export type getUsersResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getUsersResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getUsersResponseSuccess = (getUsersResponse200) & { + headers: Headers; +}; +export type getUsersResponseError = (getUsersResponse401 | getUsersResponse500) & { + headers: Headers; +}; + +export type getUsersResponse = (getUsersResponseSuccess | getUsersResponseError) + +export const getGetUsersUrl = () => { + + + + + return `/api/v1/admin/users` +} + +export const getUsers = async ( options?: RequestInit): Promise => { + + const res = await fetch(getGetUsersUrl(), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getUsersResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getUsersResponse +} + + + + + +export const getGetUsersQueryKey = () => { + return [ + `/api/v1/admin/users` + ] as const; + } + + +export const getGetUsersQueryOptions = >, TError = ErrorResponse>( options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetUsersQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getUsers({ signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetUsersQueryResult = NonNullable>> +export type GetUsersQueryError = ErrorResponse + + +export function useGetUsers>, TError = ErrorResponse>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetUsers>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetUsers>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get all users + */ + +export function useGetUsers>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetUsersQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Create, update, or delete user + */ +export type updateUserResponse200 = { + data: UsersResponse + status: 200 +} + +export type updateUserResponse400 = { + data: ErrorResponse + status: 400 +} + +export type updateUserResponse401 = { + data: ErrorResponse + status: 401 +} + +export type updateUserResponse500 = { + data: ErrorResponse + status: 500 +} + +export type updateUserResponseSuccess = (updateUserResponse200) & { + headers: Headers; +}; +export type updateUserResponseError = (updateUserResponse400 | updateUserResponse401 | updateUserResponse500) & { + headers: Headers; +}; + +export type updateUserResponse = (updateUserResponseSuccess | updateUserResponseError) + +export const getUpdateUserUrl = () => { + + + + + return `/api/v1/admin/users` +} + +export const updateUser = async (updateUserBody: UpdateUserBody, options?: RequestInit): Promise => { + const formUrlEncoded = new URLSearchParams(); +formUrlEncoded.append(`operation`, updateUserBody.operation); +formUrlEncoded.append(`user`, updateUserBody.user); +if(updateUserBody.password !== undefined) { + formUrlEncoded.append(`password`, updateUserBody.password); + } +if(updateUserBody.is_admin !== undefined) { + formUrlEncoded.append(`is_admin`, updateUserBody.is_admin.toString()) + } + + const res = await fetch(getUpdateUserUrl(), + { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...options?.headers }, + body: + formUrlEncoded, + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: updateUserResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as updateUserResponse +} + + + + +export const getUpdateUserMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: UpdateUserBody}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: UpdateUserBody}, TContext> => { + +const mutationKey = ['updateUser']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: UpdateUserBody}> = (props) => { + const {data} = props ?? {}; + + return updateUser(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type UpdateUserMutationResult = NonNullable>> + export type UpdateUserMutationBody = UpdateUserBody + export type UpdateUserMutationError = ErrorResponse + + /** + * @summary Create, update, or delete user + */ +export const useUpdateUser = (options?: { mutation?:UseMutationOptions>, TError,{data: UpdateUserBody}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: UpdateUserBody}, + TContext + > => { + return useMutation(getUpdateUserMutationOptions(options), queryClient); + } + +/** + * @summary Get import directory list + */ +export type getImportDirectoryResponse200 = { + data: DirectoryListResponse + status: 200 +} + +export type getImportDirectoryResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getImportDirectoryResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getImportDirectoryResponseSuccess = (getImportDirectoryResponse200) & { + headers: Headers; +}; +export type getImportDirectoryResponseError = (getImportDirectoryResponse401 | getImportDirectoryResponse500) & { + headers: Headers; +}; + +export type getImportDirectoryResponse = (getImportDirectoryResponseSuccess | getImportDirectoryResponseError) + +export const getGetImportDirectoryUrl = (params?: GetImportDirectoryParams,) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()) + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 ? `/api/v1/admin/import?${stringifiedParams}` : `/api/v1/admin/import` +} + +export const getImportDirectory = async (params?: GetImportDirectoryParams, options?: RequestInit): Promise => { + + const res = await fetch(getGetImportDirectoryUrl(params), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getImportDirectoryResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getImportDirectoryResponse +} + + + + + +export const getGetImportDirectoryQueryKey = (params?: GetImportDirectoryParams,) => { + return [ + `/api/v1/admin/import`, ...(params ? [params] : []) + ] as const; + } + + +export const getGetImportDirectoryQueryOptions = >, TError = ErrorResponse>(params?: GetImportDirectoryParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetImportDirectoryQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getImportDirectory(params, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetImportDirectoryQueryResult = NonNullable>> +export type GetImportDirectoryQueryError = ErrorResponse + + +export function useGetImportDirectory>, TError = ErrorResponse>( + params: undefined | GetImportDirectoryParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetImportDirectory>, TError = ErrorResponse>( + params?: GetImportDirectoryParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetImportDirectory>, TError = ErrorResponse>( + params?: GetImportDirectoryParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get import directory list + */ + +export function useGetImportDirectory>, TError = ErrorResponse>( + params?: GetImportDirectoryParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetImportDirectoryQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Perform import + */ +export type postImportResponse200 = { + data: ImportResultsResponse + status: 200 +} + +export type postImportResponse400 = { + data: ErrorResponse + status: 400 +} + +export type postImportResponse401 = { + data: ErrorResponse + status: 401 +} + +export type postImportResponse500 = { + data: ErrorResponse + status: 500 +} + +export type postImportResponseSuccess = (postImportResponse200) & { + headers: Headers; +}; +export type postImportResponseError = (postImportResponse400 | postImportResponse401 | postImportResponse500) & { + headers: Headers; +}; + +export type postImportResponse = (postImportResponseSuccess | postImportResponseError) + +export const getPostImportUrl = () => { + + + + + return `/api/v1/admin/import` +} + +export const postImport = async (postImportBody: PostImportBody, options?: RequestInit): Promise => { + const formUrlEncoded = new URLSearchParams(); +formUrlEncoded.append(`directory`, postImportBody.directory); +formUrlEncoded.append(`type`, postImportBody.type); + + const res = await fetch(getPostImportUrl(), + { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...options?.headers }, + body: + formUrlEncoded, + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: postImportResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as postImportResponse +} + + + + +export const getPostImportMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: PostImportBody}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: PostImportBody}, TContext> => { + +const mutationKey = ['postImport']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: PostImportBody}> = (props) => { + const {data} = props ?? {}; + + return postImport(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type PostImportMutationResult = NonNullable>> + export type PostImportMutationBody = PostImportBody + export type PostImportMutationError = ErrorResponse + + /** + * @summary Perform import + */ +export const usePostImport = (options?: { mutation?:UseMutationOptions>, TError,{data: PostImportBody}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: PostImportBody}, + TContext + > => { + return useMutation(getPostImportMutationOptions(options), queryClient); + } + +/** + * @summary Get import results + */ +export type getImportResultsResponse200 = { + data: ImportResultsResponse + status: 200 +} + +export type getImportResultsResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getImportResultsResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getImportResultsResponseSuccess = (getImportResultsResponse200) & { + headers: Headers; +}; +export type getImportResultsResponseError = (getImportResultsResponse401 | getImportResultsResponse500) & { + headers: Headers; +}; + +export type getImportResultsResponse = (getImportResultsResponseSuccess | getImportResultsResponseError) + +export const getGetImportResultsUrl = () => { + + + + + return `/api/v1/admin/import-results` +} + +export const getImportResults = async ( options?: RequestInit): Promise => { + + const res = await fetch(getGetImportResultsUrl(), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getImportResultsResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getImportResultsResponse +} + + + + + +export const getGetImportResultsQueryKey = () => { + return [ + `/api/v1/admin/import-results` + ] as const; + } + + +export const getGetImportResultsQueryOptions = >, TError = ErrorResponse>( options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetImportResultsQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getImportResults({ signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetImportResultsQueryResult = NonNullable>> +export type GetImportResultsQueryError = ErrorResponse + + +export function useGetImportResults>, TError = ErrorResponse>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetImportResults>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetImportResults>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get import results + */ + +export function useGetImportResults>, TError = ErrorResponse>( + options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetImportResultsQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + +/** + * @summary Get logs with optional filter + */ +export type getLogsResponse200 = { + data: LogsResponse + status: 200 +} + +export type getLogsResponse401 = { + data: ErrorResponse + status: 401 +} + +export type getLogsResponse500 = { + data: ErrorResponse + status: 500 +} + +export type getLogsResponseSuccess = (getLogsResponse200) & { + headers: Headers; +}; +export type getLogsResponseError = (getLogsResponse401 | getLogsResponse500) & { + headers: Headers; +}; + +export type getLogsResponse = (getLogsResponseSuccess | getLogsResponseError) + +export const getGetLogsUrl = (params?: GetLogsParams,) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()) + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 ? `/api/v1/admin/logs?${stringifiedParams}` : `/api/v1/admin/logs` +} + +export const getLogs = async (params?: GetLogsParams, options?: RequestInit): Promise => { + + const res = await fetch(getGetLogsUrl(params), + { + ...options, + method: 'GET' + + + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getLogsResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as getLogsResponse +} + + + + + +export const getGetLogsQueryKey = (params?: GetLogsParams,) => { + return [ + `/api/v1/admin/logs`, ...(params ? [params] : []) + ] as const; + } + + +export const getGetLogsQueryOptions = >, TError = ErrorResponse>(params?: GetLogsParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +) => { + +const {query: queryOptions, fetch: fetchOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetLogsQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getLogs(params, { signal, ...fetchOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetLogsQueryResult = NonNullable>> +export type GetLogsQueryError = ErrorResponse + + +export function useGetLogs>, TError = ErrorResponse>( + params: undefined | GetLogsParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetLogs>, TError = ErrorResponse>( + params?: GetLogsParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetLogs>, TError = ErrorResponse>( + params?: GetLogsParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get logs with optional filter + */ + +export function useGetLogs>, TError = ErrorResponse>( + params?: GetLogsParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetLogsQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + + + + + diff --git a/frontend/src/generated/model/activity.ts b/frontend/src/generated/model/activity.ts new file mode 100644 index 0000000..582276e --- /dev/null +++ b/frontend/src/generated/model/activity.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface Activity { + document_id: string; + device_id: string; + start_time: string; + title?: string; + author?: string; + duration: number; + start_percentage: number; + end_percentage: number; + read_percentage: number; +} diff --git a/frontend/src/generated/model/activityResponse.ts b/frontend/src/generated/model/activityResponse.ts new file mode 100644 index 0000000..14032e6 --- /dev/null +++ b/frontend/src/generated/model/activityResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Activity } from './activity'; + +export interface ActivityResponse { + activities: Activity[]; +} diff --git a/frontend/src/generated/model/backupType.ts b/frontend/src/generated/model/backupType.ts new file mode 100644 index 0000000..735090a --- /dev/null +++ b/frontend/src/generated/model/backupType.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type BackupType = typeof BackupType[keyof typeof BackupType]; + + +export const BackupType = { + COVERS: 'COVERS', + DOCUMENTS: 'DOCUMENTS', +} as const; diff --git a/frontend/src/generated/model/configResponse.ts b/frontend/src/generated/model/configResponse.ts new file mode 100644 index 0000000..b79f930 --- /dev/null +++ b/frontend/src/generated/model/configResponse.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface ConfigResponse { + version: string; + search_enabled: boolean; + registration_enabled: boolean; +} diff --git a/frontend/src/generated/model/createActivityItem.ts b/frontend/src/generated/model/createActivityItem.ts new file mode 100644 index 0000000..cb6ad46 --- /dev/null +++ b/frontend/src/generated/model/createActivityItem.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface CreateActivityItem { + document_id: string; + start_time: number; + duration: number; + page: number; + pages: number; +} diff --git a/frontend/src/generated/model/createActivityRequest.ts b/frontend/src/generated/model/createActivityRequest.ts new file mode 100644 index 0000000..9b0ed02 --- /dev/null +++ b/frontend/src/generated/model/createActivityRequest.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { CreateActivityItem } from './createActivityItem'; + +export interface CreateActivityRequest { + device_id: string; + device_name: string; + activity: CreateActivityItem[]; +} diff --git a/frontend/src/generated/model/createActivityResponse.ts b/frontend/src/generated/model/createActivityResponse.ts new file mode 100644 index 0000000..d954dc1 --- /dev/null +++ b/frontend/src/generated/model/createActivityResponse.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface CreateActivityResponse { + added: number; +} diff --git a/frontend/src/generated/model/createDocumentBody.ts b/frontend/src/generated/model/createDocumentBody.ts new file mode 100644 index 0000000..c5e5553 --- /dev/null +++ b/frontend/src/generated/model/createDocumentBody.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type CreateDocumentBody = { + document_file: Blob; +}; diff --git a/frontend/src/generated/model/databaseInfo.ts b/frontend/src/generated/model/databaseInfo.ts new file mode 100644 index 0000000..4103c6d --- /dev/null +++ b/frontend/src/generated/model/databaseInfo.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface DatabaseInfo { + documents_size: number; + activity_size: number; + progress_size: number; + devices_size: number; +} diff --git a/frontend/src/generated/model/device.ts b/frontend/src/generated/model/device.ts new file mode 100644 index 0000000..8c340d7 --- /dev/null +++ b/frontend/src/generated/model/device.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface Device { + id?: string; + device_name?: string; + created_at?: string; + last_synced?: string; +} diff --git a/frontend/src/generated/model/directoryItem.ts b/frontend/src/generated/model/directoryItem.ts new file mode 100644 index 0000000..412416d --- /dev/null +++ b/frontend/src/generated/model/directoryItem.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface DirectoryItem { + name?: string; + path?: string; +} diff --git a/frontend/src/generated/model/directoryListResponse.ts b/frontend/src/generated/model/directoryListResponse.ts new file mode 100644 index 0000000..ab971ba --- /dev/null +++ b/frontend/src/generated/model/directoryListResponse.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { DirectoryItem } from './directoryItem'; + +export interface DirectoryListResponse { + current_path?: string; + items?: DirectoryItem[]; +} diff --git a/frontend/src/generated/model/document.ts b/frontend/src/generated/model/document.ts new file mode 100644 index 0000000..881e804 --- /dev/null +++ b/frontend/src/generated/model/document.ts @@ -0,0 +1,26 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface Document { + id: string; + title: string; + author: string; + description?: string; + isbn10?: string; + isbn13?: string; + created_at: string; + updated_at: string; + deleted: boolean; + words?: number; + filepath?: string; + percentage?: number; + total_time_seconds?: number; + wpm?: number; + seconds_per_percent?: number; + last_read?: string; +} diff --git a/frontend/src/generated/model/documentResponse.ts b/frontend/src/generated/model/documentResponse.ts new file mode 100644 index 0000000..2eb321d --- /dev/null +++ b/frontend/src/generated/model/documentResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Document } from './document'; + +export interface DocumentResponse { + document: Document; +} diff --git a/frontend/src/generated/model/documentsResponse.ts b/frontend/src/generated/model/documentsResponse.ts new file mode 100644 index 0000000..62db32a --- /dev/null +++ b/frontend/src/generated/model/documentsResponse.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Document } from './document'; + +export interface DocumentsResponse { + documents: Document[]; + total: number; + page: number; + limit: number; + next_page?: number; + previous_page?: number; + search?: string; +} diff --git a/frontend/src/generated/model/editDocumentBody.ts b/frontend/src/generated/model/editDocumentBody.ts new file mode 100644 index 0000000..99834c6 --- /dev/null +++ b/frontend/src/generated/model/editDocumentBody.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type EditDocumentBody = { + title?: string; + author?: string; + description?: string; + isbn10?: string; + isbn13?: string; + cover_gbid?: string; +}; diff --git a/frontend/src/generated/model/errorResponse.ts b/frontend/src/generated/model/errorResponse.ts new file mode 100644 index 0000000..065705c --- /dev/null +++ b/frontend/src/generated/model/errorResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface ErrorResponse { + code: number; + message: string; +} diff --git a/frontend/src/generated/model/getActivityParams.ts b/frontend/src/generated/model/getActivityParams.ts new file mode 100644 index 0000000..d89aa21 --- /dev/null +++ b/frontend/src/generated/model/getActivityParams.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type GetActivityParams = { +doc_filter?: boolean; +document_id?: string; +offset?: number; +limit?: number; +}; diff --git a/frontend/src/generated/model/getAdmin200.ts b/frontend/src/generated/model/getAdmin200.ts new file mode 100644 index 0000000..1537da1 --- /dev/null +++ b/frontend/src/generated/model/getAdmin200.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { DatabaseInfo } from './databaseInfo'; + +export type GetAdmin200 = { + database_info?: DatabaseInfo; +}; diff --git a/frontend/src/generated/model/getDocumentsParams.ts b/frontend/src/generated/model/getDocumentsParams.ts new file mode 100644 index 0000000..ac1a5aa --- /dev/null +++ b/frontend/src/generated/model/getDocumentsParams.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type GetDocumentsParams = { +page?: number; +limit?: number; +search?: string; +}; diff --git a/frontend/src/generated/model/getImportDirectoryParams.ts b/frontend/src/generated/model/getImportDirectoryParams.ts new file mode 100644 index 0000000..bf4204a --- /dev/null +++ b/frontend/src/generated/model/getImportDirectoryParams.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type GetImportDirectoryParams = { +directory?: string; +select?: string; +}; diff --git a/frontend/src/generated/model/getLogsParams.ts b/frontend/src/generated/model/getLogsParams.ts new file mode 100644 index 0000000..d8497e5 --- /dev/null +++ b/frontend/src/generated/model/getLogsParams.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type GetLogsParams = { +filter?: string; +/** + * @minimum 1 + */ +page?: number; +/** + * @minimum 1 + */ +limit?: number; +}; diff --git a/frontend/src/generated/model/getProgressListParams.ts b/frontend/src/generated/model/getProgressListParams.ts new file mode 100644 index 0000000..d1fa3eb --- /dev/null +++ b/frontend/src/generated/model/getProgressListParams.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type GetProgressListParams = { +page?: number; +limit?: number; +document?: string; +}; diff --git a/frontend/src/generated/model/getSearchParams.ts b/frontend/src/generated/model/getSearchParams.ts new file mode 100644 index 0000000..ae09c25 --- /dev/null +++ b/frontend/src/generated/model/getSearchParams.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { GetSearchSource } from './getSearchSource'; + +export type GetSearchParams = { +query: string; +source: GetSearchSource; +}; diff --git a/frontend/src/generated/model/getSearchSource.ts b/frontend/src/generated/model/getSearchSource.ts new file mode 100644 index 0000000..cbafe55 --- /dev/null +++ b/frontend/src/generated/model/getSearchSource.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type GetSearchSource = typeof GetSearchSource[keyof typeof GetSearchSource]; + + +export const GetSearchSource = { + LibGen: 'LibGen', + Annas_Archive: 'Annas Archive', +} as const; diff --git a/frontend/src/generated/model/graphDataPoint.ts b/frontend/src/generated/model/graphDataPoint.ts new file mode 100644 index 0000000..2cbfa8f --- /dev/null +++ b/frontend/src/generated/model/graphDataPoint.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface GraphDataPoint { + date: string; + minutes_read: number; +} diff --git a/frontend/src/generated/model/graphDataResponse.ts b/frontend/src/generated/model/graphDataResponse.ts new file mode 100644 index 0000000..bdee228 --- /dev/null +++ b/frontend/src/generated/model/graphDataResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { GraphDataPoint } from './graphDataPoint'; + +export interface GraphDataResponse { + graph_data: GraphDataPoint[]; +} diff --git a/frontend/src/generated/model/homeResponse.ts b/frontend/src/generated/model/homeResponse.ts new file mode 100644 index 0000000..37b28d1 --- /dev/null +++ b/frontend/src/generated/model/homeResponse.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { DatabaseInfo } from './databaseInfo'; +import type { GraphDataResponse } from './graphDataResponse'; +import type { StreaksResponse } from './streaksResponse'; +import type { UserStatisticsResponse } from './userStatisticsResponse'; + +export interface HomeResponse { + database_info: DatabaseInfo; + streaks: StreaksResponse; + graph_data: GraphDataResponse; + user_statistics: UserStatisticsResponse; +} diff --git a/frontend/src/generated/model/importResult.ts b/frontend/src/generated/model/importResult.ts new file mode 100644 index 0000000..fee58ad --- /dev/null +++ b/frontend/src/generated/model/importResult.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { ImportResultStatus } from './importResultStatus'; + +export interface ImportResult { + id?: string; + name?: string; + path?: string; + status?: ImportResultStatus; + error?: string; +} diff --git a/frontend/src/generated/model/importResultStatus.ts b/frontend/src/generated/model/importResultStatus.ts new file mode 100644 index 0000000..935c8e2 --- /dev/null +++ b/frontend/src/generated/model/importResultStatus.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type ImportResultStatus = typeof ImportResultStatus[keyof typeof ImportResultStatus]; + + +export const ImportResultStatus = { + FAILED: 'FAILED', + SUCCESS: 'SUCCESS', + EXISTS: 'EXISTS', +} as const; diff --git a/frontend/src/generated/model/importResultsResponse.ts b/frontend/src/generated/model/importResultsResponse.ts new file mode 100644 index 0000000..4900b9a --- /dev/null +++ b/frontend/src/generated/model/importResultsResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { ImportResult } from './importResult'; + +export interface ImportResultsResponse { + results?: ImportResult[]; +} diff --git a/frontend/src/generated/model/importType.ts b/frontend/src/generated/model/importType.ts new file mode 100644 index 0000000..b8ca1f1 --- /dev/null +++ b/frontend/src/generated/model/importType.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type ImportType = typeof ImportType[keyof typeof ImportType]; + + +export const ImportType = { + DIRECT: 'DIRECT', + COPY: 'COPY', +} as const; diff --git a/frontend/src/generated/model/index.ts b/frontend/src/generated/model/index.ts new file mode 100644 index 0000000..9a4cba4 --- /dev/null +++ b/frontend/src/generated/model/index.ts @@ -0,0 +1,73 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export * from './activity'; +export * from './activityResponse'; +export * from './backupType'; +export * from './configResponse'; +export * from './createActivityItem'; +export * from './createActivityRequest'; +export * from './createActivityResponse'; +export * from './createDocumentBody'; +export * from './databaseInfo'; +export * from './device'; +export * from './directoryItem'; +export * from './directoryListResponse'; +export * from './document'; +export * from './documentResponse'; +export * from './documentsResponse'; +export * from './editDocumentBody'; +export * from './errorResponse'; +export * from './getActivityParams'; +export * from './getAdmin200'; +export * from './getDocumentsParams'; +export * from './getImportDirectoryParams'; +export * from './getLogsParams'; +export * from './getProgressListParams'; +export * from './getSearchParams'; +export * from './getSearchSource'; +export * from './graphDataPoint'; +export * from './graphDataResponse'; +export * from './homeResponse'; +export * from './importResult'; +export * from './importResultsResponse'; +export * from './importResultStatus'; +export * from './importType'; +export * from './infoResponse'; +export * from './leaderboardData'; +export * from './leaderboardEntry'; +export * from './logEntry'; +export * from './loginRequest'; +export * from './loginResponse'; +export * from './logsResponse'; +export * from './messageResponse'; +export * from './operationType'; +export * from './postAdminActionBody'; +export * from './postAdminActionBodyAction'; +export * from './postImportBody'; +export * from './postSearchBody'; +export * from './progress'; +export * from './progressListResponse'; +export * from './progressResponse'; +export * from './searchItem'; +export * from './searchResponse'; +export * from './setting'; +export * from './settingsResponse'; +export * from './streaksResponse'; +export * from './updateDocumentBody'; +export * from './updateProgressRequest'; +export * from './updateProgressResponse'; +export * from './updateSettingsRequest'; +export * from './updateUserBody'; +export * from './uploadDocumentCoverBody'; +export * from './user'; +export * from './userData'; +export * from './usersResponse'; +export * from './userStatisticsResponse'; +export * from './userStreak'; +export * from './wordCount'; \ No newline at end of file diff --git a/frontend/src/generated/model/infoResponse.ts b/frontend/src/generated/model/infoResponse.ts new file mode 100644 index 0000000..50c4e7e --- /dev/null +++ b/frontend/src/generated/model/infoResponse.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface InfoResponse { + version: string; + search_enabled: boolean; + registration_enabled: boolean; +} diff --git a/frontend/src/generated/model/leaderboardData.ts b/frontend/src/generated/model/leaderboardData.ts new file mode 100644 index 0000000..88f768e --- /dev/null +++ b/frontend/src/generated/model/leaderboardData.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { LeaderboardEntry } from './leaderboardEntry'; + +export interface LeaderboardData { + all: LeaderboardEntry[]; + year: LeaderboardEntry[]; + month: LeaderboardEntry[]; + week: LeaderboardEntry[]; +} diff --git a/frontend/src/generated/model/leaderboardEntry.ts b/frontend/src/generated/model/leaderboardEntry.ts new file mode 100644 index 0000000..3d7c56e --- /dev/null +++ b/frontend/src/generated/model/leaderboardEntry.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface LeaderboardEntry { + user_id: string; + value: number; +} diff --git a/frontend/src/generated/model/logEntry.ts b/frontend/src/generated/model/logEntry.ts new file mode 100644 index 0000000..622d4b6 --- /dev/null +++ b/frontend/src/generated/model/logEntry.ts @@ -0,0 +1,9 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type LogEntry = string; diff --git a/frontend/src/generated/model/loginRequest.ts b/frontend/src/generated/model/loginRequest.ts new file mode 100644 index 0000000..6bdc5f1 --- /dev/null +++ b/frontend/src/generated/model/loginRequest.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface LoginRequest { + username: string; + password: string; +} diff --git a/frontend/src/generated/model/loginResponse.ts b/frontend/src/generated/model/loginResponse.ts new file mode 100644 index 0000000..f7f66a0 --- /dev/null +++ b/frontend/src/generated/model/loginResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface LoginResponse { + username: string; + is_admin: boolean; +} diff --git a/frontend/src/generated/model/logsResponse.ts b/frontend/src/generated/model/logsResponse.ts new file mode 100644 index 0000000..d4f855d --- /dev/null +++ b/frontend/src/generated/model/logsResponse.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { LogEntry } from './logEntry'; + +export interface LogsResponse { + logs?: LogEntry[]; + filter?: string; + page?: number; + limit?: number; + next_page?: number; + previous_page?: number; + total?: number; +} diff --git a/frontend/src/generated/model/messageResponse.ts b/frontend/src/generated/model/messageResponse.ts new file mode 100644 index 0000000..e5b3084 --- /dev/null +++ b/frontend/src/generated/model/messageResponse.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface MessageResponse { + message: string; +} diff --git a/frontend/src/generated/model/operationType.ts b/frontend/src/generated/model/operationType.ts new file mode 100644 index 0000000..5dc84e1 --- /dev/null +++ b/frontend/src/generated/model/operationType.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type OperationType = typeof OperationType[keyof typeof OperationType]; + + +export const OperationType = { + CREATE: 'CREATE', + UPDATE: 'UPDATE', + DELETE: 'DELETE', +} as const; diff --git a/frontend/src/generated/model/postAdminActionBody.ts b/frontend/src/generated/model/postAdminActionBody.ts new file mode 100644 index 0000000..6b96959 --- /dev/null +++ b/frontend/src/generated/model/postAdminActionBody.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { BackupType } from './backupType'; +import type { PostAdminActionBodyAction } from './postAdminActionBodyAction'; + +export type PostAdminActionBody = { + action: PostAdminActionBodyAction; + backup_types?: BackupType[]; + restore_file?: Blob; +}; diff --git a/frontend/src/generated/model/postAdminActionBodyAction.ts b/frontend/src/generated/model/postAdminActionBodyAction.ts new file mode 100644 index 0000000..d2c1b83 --- /dev/null +++ b/frontend/src/generated/model/postAdminActionBodyAction.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type PostAdminActionBodyAction = typeof PostAdminActionBodyAction[keyof typeof PostAdminActionBodyAction]; + + +export const PostAdminActionBodyAction = { + BACKUP: 'BACKUP', + RESTORE: 'RESTORE', + METADATA_MATCH: 'METADATA_MATCH', + CACHE_TABLES: 'CACHE_TABLES', +} as const; diff --git a/frontend/src/generated/model/postImportBody.ts b/frontend/src/generated/model/postImportBody.ts new file mode 100644 index 0000000..561c27f --- /dev/null +++ b/frontend/src/generated/model/postImportBody.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { ImportType } from './importType'; + +export type PostImportBody = { + directory: string; + type: ImportType; +}; diff --git a/frontend/src/generated/model/postSearchBody.ts b/frontend/src/generated/model/postSearchBody.ts new file mode 100644 index 0000000..c02a8a5 --- /dev/null +++ b/frontend/src/generated/model/postSearchBody.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type PostSearchBody = { + source: string; + title: string; + author: string; + id: string; +}; diff --git a/frontend/src/generated/model/progress.ts b/frontend/src/generated/model/progress.ts new file mode 100644 index 0000000..3a8403e --- /dev/null +++ b/frontend/src/generated/model/progress.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface Progress { + title?: string; + author?: string; + device_name?: string; + device_id?: string; + percentage?: number; + progress?: string; + document_id?: string; + user_id?: string; + created_at?: string; +} diff --git a/frontend/src/generated/model/progressListResponse.ts b/frontend/src/generated/model/progressListResponse.ts new file mode 100644 index 0000000..770e293 --- /dev/null +++ b/frontend/src/generated/model/progressListResponse.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Progress } from './progress'; + +export interface ProgressListResponse { + progress?: Progress[]; + page?: number; + limit?: number; + next_page?: number; + previous_page?: number; + total?: number; +} diff --git a/frontend/src/generated/model/progressResponse.ts b/frontend/src/generated/model/progressResponse.ts new file mode 100644 index 0000000..6e45bae --- /dev/null +++ b/frontend/src/generated/model/progressResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Progress } from './progress'; + +export interface ProgressResponse { + progress?: Progress; +} diff --git a/frontend/src/generated/model/searchItem.ts b/frontend/src/generated/model/searchItem.ts new file mode 100644 index 0000000..e354f36 --- /dev/null +++ b/frontend/src/generated/model/searchItem.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface SearchItem { + id?: string; + title?: string; + author?: string; + language?: string; + series?: string; + file_type?: string; + file_size?: string; + upload_date?: string; +} diff --git a/frontend/src/generated/model/searchResponse.ts b/frontend/src/generated/model/searchResponse.ts new file mode 100644 index 0000000..b52197d --- /dev/null +++ b/frontend/src/generated/model/searchResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { SearchItem } from './searchItem'; + +export interface SearchResponse { + results: SearchItem[]; + source: string; + query: string; +} diff --git a/frontend/src/generated/model/setting.ts b/frontend/src/generated/model/setting.ts new file mode 100644 index 0000000..54b8c50 --- /dev/null +++ b/frontend/src/generated/model/setting.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface Setting { + id: string; + user_id: string; + key: string; + value: string; +} diff --git a/frontend/src/generated/model/settingsResponse.ts b/frontend/src/generated/model/settingsResponse.ts new file mode 100644 index 0000000..1140fba --- /dev/null +++ b/frontend/src/generated/model/settingsResponse.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { Device } from './device'; +import type { UserData } from './userData'; + +export interface SettingsResponse { + user: UserData; + timezone?: string; + devices?: Device[]; +} diff --git a/frontend/src/generated/model/streaksResponse.ts b/frontend/src/generated/model/streaksResponse.ts new file mode 100644 index 0000000..0492a9a --- /dev/null +++ b/frontend/src/generated/model/streaksResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { UserStreak } from './userStreak'; + +export interface StreaksResponse { + streaks: UserStreak[]; +} diff --git a/frontend/src/generated/model/updateDocumentBody.ts b/frontend/src/generated/model/updateDocumentBody.ts new file mode 100644 index 0000000..6016673 --- /dev/null +++ b/frontend/src/generated/model/updateDocumentBody.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type UpdateDocumentBody = { + title?: string; + author?: string; + description?: string; + isbn10?: string; + isbn13?: string; +}; diff --git a/frontend/src/generated/model/updateProgressRequest.ts b/frontend/src/generated/model/updateProgressRequest.ts new file mode 100644 index 0000000..0c7f8b3 --- /dev/null +++ b/frontend/src/generated/model/updateProgressRequest.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface UpdateProgressRequest { + document_id: string; + percentage: number; + progress: string; + device_id: string; + device_name: string; +} diff --git a/frontend/src/generated/model/updateProgressResponse.ts b/frontend/src/generated/model/updateProgressResponse.ts new file mode 100644 index 0000000..510827f --- /dev/null +++ b/frontend/src/generated/model/updateProgressResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface UpdateProgressResponse { + document_id: string; + timestamp: string; +} diff --git a/frontend/src/generated/model/updateSettingsRequest.ts b/frontend/src/generated/model/updateSettingsRequest.ts new file mode 100644 index 0000000..a3a7f92 --- /dev/null +++ b/frontend/src/generated/model/updateSettingsRequest.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface UpdateSettingsRequest { + password?: string; + new_password?: string; + timezone?: string; +} diff --git a/frontend/src/generated/model/updateUserBody.ts b/frontend/src/generated/model/updateUserBody.ts new file mode 100644 index 0000000..6eb69c1 --- /dev/null +++ b/frontend/src/generated/model/updateUserBody.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { OperationType } from './operationType'; + +export type UpdateUserBody = { + operation: OperationType; + user: string; + password?: string; + is_admin?: boolean; +}; diff --git a/frontend/src/generated/model/uploadDocumentCoverBody.ts b/frontend/src/generated/model/uploadDocumentCoverBody.ts new file mode 100644 index 0000000..7b9aff9 --- /dev/null +++ b/frontend/src/generated/model/uploadDocumentCoverBody.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export type UploadDocumentCoverBody = { + cover_file: Blob; +}; diff --git a/frontend/src/generated/model/user.ts b/frontend/src/generated/model/user.ts new file mode 100644 index 0000000..aa35785 --- /dev/null +++ b/frontend/src/generated/model/user.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface User { + id: string; + admin: boolean; + created_at: string; +} diff --git a/frontend/src/generated/model/userData.ts b/frontend/src/generated/model/userData.ts new file mode 100644 index 0000000..4fbc738 --- /dev/null +++ b/frontend/src/generated/model/userData.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface UserData { + username: string; + is_admin: boolean; +} diff --git a/frontend/src/generated/model/userStatisticsResponse.ts b/frontend/src/generated/model/userStatisticsResponse.ts new file mode 100644 index 0000000..888f950 --- /dev/null +++ b/frontend/src/generated/model/userStatisticsResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { LeaderboardData } from './leaderboardData'; + +export interface UserStatisticsResponse { + wpm: LeaderboardData; + duration: LeaderboardData; + words: LeaderboardData; +} diff --git a/frontend/src/generated/model/userStreak.ts b/frontend/src/generated/model/userStreak.ts new file mode 100644 index 0000000..04473ba --- /dev/null +++ b/frontend/src/generated/model/userStreak.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface UserStreak { + window: string; + max_streak: number; + max_streak_start_date: string; + max_streak_end_date: string; + current_streak: number; + current_streak_start_date: string; + current_streak_end_date: string; +} diff --git a/frontend/src/generated/model/usersResponse.ts b/frontend/src/generated/model/usersResponse.ts new file mode 100644 index 0000000..c0d4174 --- /dev/null +++ b/frontend/src/generated/model/usersResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { User } from './user'; + +export interface UsersResponse { + users?: User[]; +} diff --git a/frontend/src/generated/model/wordCount.ts b/frontend/src/generated/model/wordCount.ts new file mode 100644 index 0000000..283f848 --- /dev/null +++ b/frontend/src/generated/model/wordCount.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface WordCount { + document_id: string; + count: number; +} diff --git a/frontend/src/hooks/useDebounce.test.tsx b/frontend/src/hooks/useDebounce.test.tsx new file mode 100644 index 0000000..c23db40 --- /dev/null +++ b/frontend/src/hooks/useDebounce.test.tsx @@ -0,0 +1,69 @@ +import { describe, expect, it, vi, afterEach } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useDebounce } from './useDebounce'; + +describe('useDebounce', () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it('returns the initial value immediately', () => { + const { result } = renderHook(({ value, delay }) => useDebounce(value, delay), { + initialProps: { value: 'initial', delay: 300 }, + }); + + expect(result.current).toBe('initial'); + }); + + it('delays updates until the debounce interval has passed', () => { + vi.useFakeTimers(); + + const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { + initialProps: { value: 'initial', delay: 300 }, + }); + + rerender({ value: 'updated', delay: 300 }); + + expect(result.current).toBe('initial'); + + act(() => { + vi.advanceTimersByTime(299); + }); + + expect(result.current).toBe('initial'); + + act(() => { + vi.advanceTimersByTime(1); + }); + + expect(result.current).toBe('updated'); + }); + + it('cancels the previous timer when the value changes again', () => { + vi.useFakeTimers(); + + const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { + initialProps: { value: 'first', delay: 300 }, + }); + + rerender({ value: 'second', delay: 300 }); + + act(() => { + vi.advanceTimersByTime(200); + }); + + rerender({ value: 'third', delay: 300 }); + + act(() => { + vi.advanceTimersByTime(100); + }); + + expect(result.current).toBe('first'); + + act(() => { + vi.advanceTimersByTime(200); + }); + + expect(result.current).toBe('third'); + }); +}); diff --git a/frontend/src/hooks/useDebounce.ts b/frontend/src/hooks/useDebounce.ts new file mode 100644 index 0000000..98d0abd --- /dev/null +++ b/frontend/src/hooks/useDebounce.ts @@ -0,0 +1,23 @@ +import { useState, useEffect } from 'react'; + +/** + * Debounces a value by delaying updates until after a specified delay + * @param value The value to debounce + * @param delay The delay in milliseconds + * @returns The debounced value + */ +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/frontend/src/hooks/useEpubReader.ts b/frontend/src/hooks/useEpubReader.ts new file mode 100644 index 0000000..47b58fa --- /dev/null +++ b/frontend/src/hooks/useEpubReader.ts @@ -0,0 +1,237 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createActivity, getGetDocumentFileUrl, updateProgress } from '../generated/anthoLumeAPIV1'; +import type { CreateActivityRequest } from '../generated/model/createActivityRequest'; +import type { UpdateProgressRequest } from '../generated/model/updateProgressRequest'; +import { EBookReader, type ReaderStats, type ReaderTocItem } from '../lib/reader/EBookReader'; +import type { ReaderColorScheme, ReaderFontFamily } from '../utils/localSettings'; + +interface UseEpubReaderOptions { + documentId: string; + initialProgress?: string; + deviceId: string; + deviceName: string; + colorScheme: ReaderColorScheme; + fontFamily: ReaderFontFamily; + fontSize: number; + isPaginationDisabled: () => boolean; + onSwipeDown: () => void; + onSwipeUp: () => void; + onCenterTap: () => void; +} + +interface UseEpubReaderResult { + viewerRef: (_node: HTMLDivElement | null) => void; + isReady: boolean; + isLoading: boolean; + error: string | null; + toc: ReaderTocItem[]; + stats: ReaderStats; + nextPage: () => Promise; + prevPage: () => Promise; + goToHref: (href: string) => Promise; + setTheme: (theme: { + colorScheme?: ReaderColorScheme; + fontFamily?: ReaderFontFamily; + fontSize?: number; + }) => Promise; +} + +export function useEpubReader({ + documentId, + initialProgress, + deviceId, + deviceName, + colorScheme, + fontFamily, + fontSize, + isPaginationDisabled, + onSwipeDown, + onSwipeUp, + onCenterTap, +}: UseEpubReaderOptions): UseEpubReaderResult { + const [viewerNode, setViewerNode] = useState(null); + const readerRef = useRef(null); + const isPaginationDisabledRef = useRef(isPaginationDisabled); + const onSwipeDownRef = useRef(onSwipeDown); + const onSwipeUpRef = useRef(onSwipeUp); + const onCenterTapRef = useRef(onCenterTap); + const [isReady, setIsReady] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [toc, setToc] = useState([]); + const [stats, setStats] = useState({ + chapterName: 'N/A', + sectionPage: 0, + sectionTotalPages: 0, + percentage: 0, + }); + + useEffect(() => { + isPaginationDisabledRef.current = isPaginationDisabled; + onSwipeDownRef.current = onSwipeDown; + onSwipeUpRef.current = onSwipeUp; + onCenterTapRef.current = onCenterTap; + }, [isPaginationDisabled, onCenterTap, onSwipeDown, onSwipeUp]); + + useEffect(() => { + const container = viewerNode; + if (!container) { + return; + } + + let isCancelled = false; + let objectUrl: string | null = null; + let reader: EBookReader | null = null; + + setIsReady(false); + setIsLoading(true); + setError(null); + setToc([]); + setStats({ + chapterName: 'N/A', + sectionPage: 0, + sectionTotalPages: 0, + percentage: 0, + }); + + const saveProgress = async (payload: UpdateProgressRequest) => { + const response = await updateProgress(payload); + if (response.status >= 400) { + throw new Error( + 'message' in response.data ? response.data.message : 'Unable to save reader progress' + ); + } + }; + + const saveActivity = async (payload: CreateActivityRequest) => { + const response = await createActivity(payload); + if (response.status >= 400) { + throw new Error( + 'message' in response.data ? response.data.message : 'Unable to save reader activity' + ); + } + }; + + const initializeReader = async () => { + try { + const response = await fetch(getGetDocumentFileUrl(documentId)); + const contentType = response.headers.get('content-type') || ''; + + if (!response.ok || contentType.includes('application/json')) { + let message = 'Unable to load document file'; + try { + const errorData = (await response.json()) as { message?: string }; + if (errorData.message) { + message = errorData.message; + } + } catch { + // ignore parse failure and use fallback message + } + throw new Error(message); + } + + const blob = await response.blob(); + if (isCancelled) { + return; + } + + objectUrl = URL.createObjectURL(blob); + reader = new EBookReader({ + container, + bookUrl: objectUrl, + documentId, + initialProgress, + deviceId, + deviceName, + colorScheme, + fontFamily, + fontSize, + onReady: () => setIsReady(true), + onLoading: loading => setIsLoading(loading), + onError: message => setError(message), + onStats: nextStats => setStats(nextStats), + onToc: nextToc => setToc(nextToc), + onSaveProgress: saveProgress, + onCreateActivity: saveActivity, + isPaginationDisabled: () => isPaginationDisabledRef.current(), + onSwipeDown: () => onSwipeDownRef.current(), + onSwipeUp: () => onSwipeUpRef.current(), + onCenterTap: () => onCenterTapRef.current(), + }); + + readerRef.current = reader; + } catch (err) { + if (isCancelled) { + return; + } + setError(err instanceof Error ? err.message : 'Unable to load document file'); + setIsLoading(false); + } + }; + + void initializeReader(); + + return () => { + isCancelled = true; + reader?.destroy(); + if (readerRef.current === reader) { + readerRef.current = null; + } + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + }; + }, [deviceId, deviceName, documentId, initialProgress, viewerNode]); + + useEffect(() => { + const reader = readerRef.current; + if (!reader || !isReady) { + return; + } + + void reader.applyThemeChange({ + colorScheme, + fontFamily, + fontSize, + }); + }, [colorScheme, fontFamily, fontSize, isReady]); + + const nextPage = useCallback(async () => { + await readerRef.current?.nextPage(); + }, []); + + const prevPage = useCallback(async () => { + await readerRef.current?.prevPage(); + }, []); + + const goToHref = useCallback(async (href: string) => { + await readerRef.current?.displayHref(href); + }, []); + + const setTheme = useCallback( + async (theme: { + colorScheme?: ReaderColorScheme; + fontFamily?: ReaderFontFamily; + fontSize?: number; + }) => { + await readerRef.current?.applyThemeChange(theme); + }, + [] + ); + + return useMemo( + () => ({ + viewerRef: setViewerNode, + isReady, + isLoading, + error, + toc, + stats, + nextPage, + prevPage, + goToHref, + setTheme, + }), + [error, goToHref, isLoading, isReady, nextPage, prevPage, setTheme, stats, toc] + ); +} diff --git a/frontend/src/icons/ActivityIcon.tsx b/frontend/src/icons/ActivityIcon.tsx new file mode 100644 index 0000000..f60572d --- /dev/null +++ b/frontend/src/icons/ActivityIcon.tsx @@ -0,0 +1,20 @@ +import { BaseIcon } from './BaseIcon'; + +interface ActivityIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function ActivityIcon({ size = 24, className = '', disabled = false }: ActivityIconProps) { + return ( + + + + + ); +} diff --git a/frontend/src/icons/AddIcon.tsx b/frontend/src/icons/AddIcon.tsx new file mode 100644 index 0000000..ae09e0f --- /dev/null +++ b/frontend/src/icons/AddIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface AddIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function AddIcon({ size = 24, className = '', disabled = false }: AddIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/BaseIcon.tsx b/frontend/src/icons/BaseIcon.tsx new file mode 100644 index 0000000..2f98e07 --- /dev/null +++ b/frontend/src/icons/BaseIcon.tsx @@ -0,0 +1,38 @@ +import { type ReactNode } from 'react'; + +interface BaseIconProps { + size?: number; + className?: string; + disabled?: boolean; + hoverable?: boolean; + viewBox?: string; + children: ReactNode; +} + +export function BaseIcon({ + size = 24, + className = '', + disabled = false, + hoverable = true, + viewBox = '0 0 24 24', + children, +}: BaseIconProps) { + const disabledClasses = disabled + ? 'text-content-subtle' + : hoverable + ? 'hover:text-content' + : ''; + + return ( + + {children} + + ); +} diff --git a/frontend/src/icons/BookIcon.tsx b/frontend/src/icons/BookIcon.tsx new file mode 100644 index 0000000..415c56e --- /dev/null +++ b/frontend/src/icons/BookIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface BookIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function BookIcon({ size = 24, className = '', disabled = false }: BookIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/CheckIcon.tsx b/frontend/src/icons/CheckIcon.tsx new file mode 100644 index 0000000..b852783 --- /dev/null +++ b/frontend/src/icons/CheckIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface CheckIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function CheckIcon({ size = 24, className = '', disabled = false }: CheckIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/ClockIcon.tsx b/frontend/src/icons/ClockIcon.tsx new file mode 100644 index 0000000..f092b63 --- /dev/null +++ b/frontend/src/icons/ClockIcon.tsx @@ -0,0 +1,21 @@ +import { BaseIcon } from './BaseIcon'; + +interface ClockIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function ClockIcon({ size = 24, className = '', disabled = false }: ClockIconProps) { + return ( + + + + + ); +} diff --git a/frontend/src/icons/CloseIcon.tsx b/frontend/src/icons/CloseIcon.tsx new file mode 100644 index 0000000..d793df3 --- /dev/null +++ b/frontend/src/icons/CloseIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface CloseIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function CloseIcon({ size = 24, className = '', disabled = false }: CloseIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/DeleteIcon.tsx b/frontend/src/icons/DeleteIcon.tsx new file mode 100644 index 0000000..1f8d675 --- /dev/null +++ b/frontend/src/icons/DeleteIcon.tsx @@ -0,0 +1,16 @@ +import { BaseIcon } from './BaseIcon'; + +interface DeleteIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function DeleteIcon({ size = 24, className = '', disabled = false }: DeleteIconProps) { + return ( + + + + + ); +} diff --git a/frontend/src/icons/DocumentsIcon.tsx b/frontend/src/icons/DocumentsIcon.tsx new file mode 100644 index 0000000..cd1f8db --- /dev/null +++ b/frontend/src/icons/DocumentsIcon.tsx @@ -0,0 +1,20 @@ +import { BaseIcon } from './BaseIcon'; + +interface DocumentsIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function DocumentsIcon({ size = 24, className = '', disabled = false }: DocumentsIconProps) { + return ( + + + + + ); +} diff --git a/frontend/src/icons/DownloadIcon.tsx b/frontend/src/icons/DownloadIcon.tsx new file mode 100644 index 0000000..0cce47a --- /dev/null +++ b/frontend/src/icons/DownloadIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface DownloadIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function DownloadIcon({ size = 24, className = '', disabled = false }: DownloadIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/DropdownIcon.tsx b/frontend/src/icons/DropdownIcon.tsx new file mode 100644 index 0000000..9abd616 --- /dev/null +++ b/frontend/src/icons/DropdownIcon.tsx @@ -0,0 +1,15 @@ +import { BaseIcon } from './BaseIcon'; + +interface DropdownIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function DropdownIcon({ size = 24, className = '', disabled = false }: DropdownIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/EditIcon.tsx b/frontend/src/icons/EditIcon.tsx new file mode 100644 index 0000000..c166c1c --- /dev/null +++ b/frontend/src/icons/EditIcon.tsx @@ -0,0 +1,17 @@ +import { BaseIcon } from './BaseIcon'; + +interface EditIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function EditIcon({ size = 24, className = '', disabled = false }: EditIconProps) { + return ( + + + + + + ); +} diff --git a/frontend/src/icons/ErrorIcon.tsx b/frontend/src/icons/ErrorIcon.tsx new file mode 100644 index 0000000..48ee784 --- /dev/null +++ b/frontend/src/icons/ErrorIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface ErrorIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function ErrorIcon({ size = 24, className = '', disabled = false }: ErrorIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/FolderOpenIcon.tsx b/frontend/src/icons/FolderOpenIcon.tsx new file mode 100644 index 0000000..d99929f --- /dev/null +++ b/frontend/src/icons/FolderOpenIcon.tsx @@ -0,0 +1,23 @@ +import { BaseIcon } from './BaseIcon'; + +interface FolderOpenIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function FolderOpenIcon({ + size = 24, + className = '', + disabled = false, +}: FolderOpenIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/GitIcon.tsx b/frontend/src/icons/GitIcon.tsx new file mode 100644 index 0000000..e03ec68 --- /dev/null +++ b/frontend/src/icons/GitIcon.tsx @@ -0,0 +1,50 @@ +interface GitIconProps { + size?: number; + className?: string; +} + +export function GitIcon({ size = 20, className = '' }: GitIconProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/icons/HomeIcon.tsx b/frontend/src/icons/HomeIcon.tsx new file mode 100644 index 0000000..ae49ce5 --- /dev/null +++ b/frontend/src/icons/HomeIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface HomeIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function HomeIcon({ size = 24, className = '', disabled = false }: HomeIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/ImportIcon.tsx b/frontend/src/icons/ImportIcon.tsx new file mode 100644 index 0000000..9bad7e6 --- /dev/null +++ b/frontend/src/icons/ImportIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface ImportIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function ImportIcon({ size = 24, className = '', disabled = false }: ImportIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/InfoIcon.tsx b/frontend/src/icons/InfoIcon.tsx new file mode 100644 index 0000000..642da51 --- /dev/null +++ b/frontend/src/icons/InfoIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface InfoIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function InfoIcon({ size = 24, className = '', disabled = false }: InfoIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/LoadingIcon.tsx b/frontend/src/icons/LoadingIcon.tsx new file mode 100644 index 0000000..8c40912 --- /dev/null +++ b/frontend/src/icons/LoadingIcon.tsx @@ -0,0 +1,51 @@ +interface LoadingIconProps { + size?: number; + className?: string; +} + +const spinnerAnimation = 'spinner_rcyq 1.2s cubic-bezier(0.52, 0.6, 0.25, 0.99) infinite'; + +const spinnerPath = 'M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z'; + +export function LoadingIcon({ size = 24, className = '' }: LoadingIconProps) { + return ( + + + + + + + ); +} diff --git a/frontend/src/icons/PasswordIcon.tsx b/frontend/src/icons/PasswordIcon.tsx new file mode 100644 index 0000000..023ec07 --- /dev/null +++ b/frontend/src/icons/PasswordIcon.tsx @@ -0,0 +1,15 @@ +import { BaseIcon } from './BaseIcon'; + +interface PasswordIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function PasswordIcon({ size = 24, className = '', disabled = false }: PasswordIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/Search2Icon.tsx b/frontend/src/icons/Search2Icon.tsx new file mode 100644 index 0000000..83ca6d5 --- /dev/null +++ b/frontend/src/icons/Search2Icon.tsx @@ -0,0 +1,21 @@ +import { BaseIcon } from './BaseIcon'; + +interface Search2IconProps { + size?: number; + className?: string; + disabled?: boolean; + hoverable?: boolean; +} + +export function Search2Icon({ size = 24, className = '', disabled = false, hoverable = true }: Search2IconProps) { + return ( + + + + + ); +} diff --git a/frontend/src/icons/SearchIcon.tsx b/frontend/src/icons/SearchIcon.tsx new file mode 100644 index 0000000..8d11774 --- /dev/null +++ b/frontend/src/icons/SearchIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface SearchIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function SearchIcon({ size = 24, className = '', disabled = false }: SearchIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/SettingsIcon.tsx b/frontend/src/icons/SettingsIcon.tsx new file mode 100644 index 0000000..d2dce28 --- /dev/null +++ b/frontend/src/icons/SettingsIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface SettingsIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function SettingsIcon({ size = 24, className = '', disabled = false }: SettingsIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/UploadIcon.tsx b/frontend/src/icons/UploadIcon.tsx new file mode 100644 index 0000000..1c309b8 --- /dev/null +++ b/frontend/src/icons/UploadIcon.tsx @@ -0,0 +1,20 @@ +import { BaseIcon } from './BaseIcon'; + +interface UploadIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function UploadIcon({ size = 24, className = '', disabled = false }: UploadIconProps) { + return ( + + + + + ); +} diff --git a/frontend/src/icons/UserIcon.tsx b/frontend/src/icons/UserIcon.tsx new file mode 100644 index 0000000..333478b --- /dev/null +++ b/frontend/src/icons/UserIcon.tsx @@ -0,0 +1,15 @@ +import { BaseIcon } from './BaseIcon'; + +interface UserIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function UserIcon({ size = 24, className = '', disabled = false }: UserIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/WarningIcon.tsx b/frontend/src/icons/WarningIcon.tsx new file mode 100644 index 0000000..bfb4663 --- /dev/null +++ b/frontend/src/icons/WarningIcon.tsx @@ -0,0 +1,19 @@ +import { BaseIcon } from './BaseIcon'; + +interface WarningIconProps { + size?: number; + className?: string; + disabled?: boolean; +} + +export function WarningIcon({ size = 24, className = '', disabled = false }: WarningIconProps) { + return ( + + + + ); +} diff --git a/frontend/src/icons/index.ts b/frontend/src/icons/index.ts new file mode 100644 index 0000000..af340ab --- /dev/null +++ b/frontend/src/icons/index.ts @@ -0,0 +1,26 @@ +export { BaseIcon } from './BaseIcon'; +export { HomeIcon } from './HomeIcon'; +export { SettingsIcon } from './SettingsIcon'; +export { SearchIcon } from './SearchIcon'; +export { ActivityIcon } from './ActivityIcon'; +export { AddIcon } from './AddIcon'; +export { UserIcon } from './UserIcon'; +export { DocumentsIcon } from './DocumentsIcon'; +export { EditIcon } from './EditIcon'; +export { DeleteIcon } from './DeleteIcon'; +export { DownloadIcon } from './DownloadIcon'; +export { UploadIcon } from './UploadIcon'; +export { ImportIcon } from './ImportIcon'; +export { InfoIcon } from './InfoIcon'; +export { Search2Icon } from './Search2Icon'; +export { DropdownIcon } from './DropdownIcon'; +export { ClockIcon } from './ClockIcon'; +export { PasswordIcon } from './PasswordIcon'; +export { LoadingIcon } from './LoadingIcon'; +export { GitIcon } from './GitIcon'; +export { WarningIcon } from './WarningIcon'; +export { ErrorIcon } from './ErrorIcon'; +export { CloseIcon } from './CloseIcon'; +export { CheckIcon } from './CheckIcon'; +export { FolderOpenIcon } from './FolderOpenIcon'; +export { BookIcon } from './BookIcon'; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..924b476 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,339 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --white: 255 255 255; + --black: 0 0 0; + + --canvas: 243 244 246; + --surface: 255 255 255; + --surface-muted: 249 250 251; + --surface-strong: 209 213 219; + --overlay: 31 41 55; + + --content: 0 0 0; + --content-muted: 107 114 128; + --content-subtle: 156 163 175; + --content-inverse: 255 255 255; + + --border: 209 213 219; + --border-muted: 229 231 235; + --border-strong: 156 163 175; + + --neutral-50: 249 250 251; + --neutral-100: 243 244 246; + --neutral-200: 229 231 235; + --neutral-300: 209 213 219; + --neutral-400: 156 163 175; + --neutral-500: 107 114 128; + --neutral-600: 75 85 99; + --neutral-700: 55 65 81; + --neutral-800: 31 41 55; + --neutral-900: 17 24 39; + + --primary-50: 250 245 255; + --primary-100: 243 232 255; + --primary-200: 233 213 255; + --primary-300: 216 180 254; + --primary-400: 192 132 252; + --primary-500: 168 85 247; + --primary-600: 147 51 234; + --primary-700: 126 34 206; + --primary-800: 107 33 168; + --primary-900: 88 28 135; + --primary-foreground: 255 255 255; + + --secondary-50: 239 246 255; + --secondary-100: 219 234 254; + --secondary-200: 191 219 254; + --secondary-300: 147 197 253; + --secondary-400: 96 165 250; + --secondary-500: 59 130 246; + --secondary-600: 37 99 235; + --secondary-700: 29 78 216; + --secondary-800: 30 64 175; + --secondary-900: 30 58 138; + --secondary-foreground: 255 255 255; + + --tertiary-50: 236 253 245; + --tertiary-100: 209 250 229; + --tertiary-200: 167 243 208; + --tertiary-300: 110 231 183; + --tertiary-400: 52 211 153; + --tertiary-500: 16 185 129; + --tertiary-600: 5 150 105; + --tertiary-700: 4 120 87; + --tertiary-800: 6 95 70; + --tertiary-900: 6 78 59; + --tertiary-foreground: 255 255 255; + + --warning-50: 254 252 232; + --warning-100: 254 249 195; + --warning-200: 254 240 138; + --warning-300: 253 224 71; + --warning-400: 250 204 21; + --warning-500: 234 179 8; + --warning-600: 202 138 4; + --warning-700: 161 98 7; + --warning-800: 133 77 14; + --warning-900: 113 63 18; + --warning-foreground: 17 24 39; + + --error-50: 254 242 242; + --error-100: 254 226 226; + --error-200: 254 202 202; + --error-300: 252 165 165; + --error-400: 248 113 113; + --error-500: 239 68 68; + --error-600: 220 38 38; + --error-700: 185 28 28; + --error-800: 153 27 27; + --error-900: 127 29 29; + --error-foreground: 255 255 255; +} + +.dark { + --white: 255 255 255; + --black: 0 0 0; + + --canvas: 31 41 55; + --surface: 55 65 81; + --surface-muted: 75 85 99; + --surface-strong: 107 114 128; + --overlay: 229 231 235; + + --content: 255 255 255; + --content-muted: 209 213 219; + --content-subtle: 156 163 175; + --content-inverse: 17 24 39; + + --border: 75 85 99; + --border-muted: 55 65 81; + --border-strong: 107 114 128; + + --neutral-50: 249 250 251; + --neutral-100: 243 244 246; + --neutral-200: 229 231 235; + --neutral-300: 209 213 219; + --neutral-400: 156 163 175; + --neutral-500: 107 114 128; + --neutral-600: 75 85 99; + --neutral-700: 55 65 81; + --neutral-800: 31 41 55; + --neutral-900: 17 24 39; + + --primary-50: 250 245 255; + --primary-100: 243 232 255; + --primary-200: 233 213 255; + --primary-300: 216 180 254; + --primary-400: 192 132 252; + --primary-500: 168 85 247; + --primary-600: 147 51 234; + --primary-700: 126 34 206; + --primary-800: 107 33 168; + --primary-900: 88 28 135; + --primary-foreground: 255 255 255; + + --secondary-50: 239 246 255; + --secondary-100: 219 234 254; + --secondary-200: 191 219 254; + --secondary-300: 147 197 253; + --secondary-400: 96 165 250; + --secondary-500: 59 130 246; + --secondary-600: 37 99 235; + --secondary-700: 29 78 216; + --secondary-800: 30 64 175; + --secondary-900: 30 58 138; + --secondary-foreground: 255 255 255; + + --tertiary-50: 236 253 245; + --tertiary-100: 209 250 229; + --tertiary-200: 167 243 208; + --tertiary-300: 110 231 183; + --tertiary-400: 52 211 153; + --tertiary-500: 16 185 129; + --tertiary-600: 5 150 105; + --tertiary-700: 4 120 87; + --tertiary-800: 6 95 70; + --tertiary-900: 6 78 59; + --tertiary-foreground: 255 255 255; + + --warning-50: 254 252 232; + --warning-100: 254 249 195; + --warning-200: 254 240 138; + --warning-300: 253 224 71; + --warning-400: 250 204 21; + --warning-500: 234 179 8; + --warning-600: 202 138 4; + --warning-700: 161 98 7; + --warning-800: 133 77 14; + --warning-900: 113 63 18; + --warning-foreground: 17 24 39; + + --error-50: 254 242 242; + --error-100: 254 226 226; + --error-200: 254 202 202; + --error-300: 252 165 165; + --error-400: 248 113 113; + --error-500: 239 68 68; + --error-600: 220 38 38; + --error-700: 185 28 28; + --error-800: 153 27 27; + --error-900: 127 29 29; + --error-foreground: 255 255 255; +} + +@layer base { + html, + body { + overscroll-behavior-y: none; + margin: 0; + } + + html { + height: calc(100% + env(safe-area-inset-bottom)); + padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left); + background-color: rgb(var(--canvas)); + } + + body { + background-color: rgb(var(--canvas)); + color: rgb(var(--content)); + transition: + background-color 150ms ease, + color 150ms ease; + } +} + +main { + height: calc(100dvh - 4rem - env(safe-area-inset-top)); +} + +#container { + padding-bottom: calc(5em + env(safe-area-inset-bottom) * 2); +} + +/* No Scrollbar - IE, Edge, Firefox */ +* { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* No Scrollbar - WebKit */ +*::-webkit-scrollbar { + display: none; +} + +/* Button visibility toggle */ +.css-button:checked + div { + visibility: visible; + opacity: 1; +} + +.css-button + div { + visibility: hidden; + opacity: 0; +} + +/* Mobile Navigation */ +#mobile-nav-button span { + transform-origin: 5px 0; + transition: + transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), + background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), + opacity 0.55s ease; +} + +#mobile-nav-button span:first-child { + transform-origin: 0 0; +} + +#mobile-nav-button span:nth-last-child(2) { + transform-origin: 0 100%; +} + +#mobile-nav-button:checked ~ span { + opacity: 1; + transform: rotate(45deg) translate(2px, -2px); +} + +#mobile-nav-button:checked ~ span:nth-last-child(3) { + opacity: 0; + transform: rotate(0deg) scale(0.2, 0.2); +} + +#mobile-nav-button:checked ~ span:nth-last-child(2) { + transform: rotate(-45deg) translate(0, 6px); +} + +#mobile-nav-button:checked ~ #menu { + transform: translate(0, 0) !important; +} + +@media (min-width: 1024px) { + #mobile-nav-button ~ #menu { + transform: none; + } +} + +#menu { + top: 0; + padding-top: env(safe-area-inset-top); + transform-origin: 0 0; + transform: translate(-100%, 0); + transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1); +} + +@media (orientation: landscape) { + #menu { + transform: translate(calc(-1 * (env(safe-area-inset-left) + 100%)), 0); + } +} + +/* Skeleton Wave Animation */ +@keyframes wave { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.animate-wave { + background: linear-gradient( + 90deg, + rgb(var(--neutral-200)) 0%, + rgb(var(--neutral-100)) 50%, + rgb(var(--neutral-200)) 100% + ); + background-size: 200% 100%; + animation: wave 1.5s ease-in-out infinite; +} + +.dark .animate-wave { + background: linear-gradient( + 90deg, + rgb(var(--neutral-600)) 0%, + rgb(var(--neutral-500)) 50%, + rgb(var(--neutral-600)) 100% + ); + background-size: 200% 100%; +} + +/* Toast Slide In Animation */ +@keyframes slideInRight { + 0% { + opacity: 0; + transform: translateX(100%); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} + +.animate-slideInRight { + animation: slideInRight 0.3s ease-out forwards; +} diff --git a/frontend/src/lib/reader/EBookReader.ts b/frontend/src/lib/reader/EBookReader.ts new file mode 100644 index 0000000..872bed4 --- /dev/null +++ b/frontend/src/lib/reader/EBookReader.ts @@ -0,0 +1,1001 @@ +import ePub from 'epubjs'; +import NoSleep from 'nosleep.js'; +import type { CreateActivityRequest } from '../../generated/model/createActivityRequest'; +import type { UpdateProgressRequest } from '../../generated/model/updateProgressRequest'; +import type { ReaderColorScheme, ReaderFontFamily } from '../../utils/localSettings'; + +const THEMES: ReaderColorScheme[] = ['light', 'tan', 'blue', 'gray', 'black']; +const THEME_FILE = '/assets/reader/themes.css'; +const FONT_FILE = '/assets/reader/fonts.css'; + +interface TocNode { + href: string; + label?: string; + subitems?: TocNode[]; +} + +interface EpubContents { + document: Document; + sectionIndex?: number; + range: (cfi: string) => Range; +} + +interface EpubVisibleSection { + index: number; + layout: { width: number; divisor: number }; + width: () => number; + expand: () => void; +} + +interface EpubLocation { + start: { + cfi: string; + href?: string; + }; + end: { + cfi: string; + }; +} + +interface EpubNavigation { + toc?: TocNode[]; +} + +interface EpubSpineItem { + cfiBase: string; + index: number; + document: Document; + load: (_loader: unknown) => Promise; + cfiFromElement: (element: Element) => string; + wordCount?: number; +} + +interface EpubBook { + ready: Promise; + navigation?: EpubNavigation; + loaded: { navigation: Promise }; + spine: { + spineItems: EpubSpineItem[]; + get: (index: number) => EpubSpineItem; + hooks: { + content: { register: (_callback: (output: Document) => void) => void }; + }; + }; + load: (...args: unknown[]) => unknown; + renderTo: (element: HTMLElement, options: Record) => EpubRendition; + getRange: (cfiRange: string) => Promise; + destroy?: () => void; +} + +interface EpubRendition { + next: () => Promise; + prev: () => Promise; + display: (target?: string) => Promise; + currentLocation: () => Promise; + getContents: () => EpubContents[]; + themes: { + default: (styles: Record) => void; + register: (name: string, styles: Record | string) => void; + select: (name: string) => void; + }; + hooks: { + content: { register: (_callback: () => void) => void }; + render: { register: (_callback: (contents: EpubContents) => void) => void }; + }; + manager?: { + visible?: () => EpubVisibleSection[]; + }; + views: () => { container: { scrollLeft: number } }; + destroy?: () => void; +} + +interface ParsedCfiPath { + steps: unknown[]; + terminal: unknown; +} + +interface ParsedCfi { + base: unknown; + path: ParsedCfiPath; +} + +interface EpubCfiHelper { + parse: (_value: string) => ParsedCfi; + equalStep: (_a: unknown, _b: unknown) => boolean; + segmentString: (_value: unknown) => string; +} + +interface EpubWithCfiConstructor { + CFI: new () => EpubCfiHelper; +} + +export interface ReaderStats { + chapterName: string; + sectionPage: number; + sectionTotalPages: number; + percentage: number; +} + +export interface ReaderTocItem { + title: string; + href: string; +} + +interface BookState { + pages: number; + percentage: number; + progress: string; + progressElement: Element | null; + readActivity: unknown[]; + words: number; + pageStart: number; +} + +interface ReaderSettings { + theme?: { + colorScheme?: ReaderColorScheme; + fontFamily?: string; + fontSize?: number; + }; +} + +interface EBookReaderOptions { + container: HTMLElement; + bookUrl: string; + documentId: string; + initialProgress?: string; + deviceId: string; + deviceName: string; + colorScheme: ReaderColorScheme; + fontFamily: ReaderFontFamily; + fontSize: number; + onReady: () => void; + onLoading: (_loading: boolean) => void; + onError: (_message: string) => void; + onStats: (_stats: ReaderStats) => void; + onToc: (_toc: ReaderTocItem[]) => void; + onSaveProgress: (_payload: UpdateProgressRequest) => Promise; + onCreateActivity: (_payload: CreateActivityRequest) => Promise; + isPaginationDisabled: () => boolean; + onSwipeDown: () => void; + onSwipeUp: () => void; + onCenterTap: () => void; +} + +export class EBookReader { + private container: HTMLElement; + private bookUrl: string; + private documentId: string; + private deviceId: string; + private deviceName: string; + private readerSettings: ReaderSettings = {}; + private bookState: BookState; + private book: EpubBook; + private rendition: EpubRendition; + private noSleep: NoSleep | null = null; + private wakeTimeoutId: ReturnType | null = null; + private destroyed = false; + private onReady: () => void; + private onLoading: (_loading: boolean) => void; + private onError: (_message: string) => void; + private onStats: (_stats: ReaderStats) => void; + private onToc: (_toc: ReaderTocItem[]) => void; + private onSaveProgress: (_payload: UpdateProgressRequest) => Promise; + private onCreateActivity: (_payload: CreateActivityRequest) => Promise; + private isPaginationDisabled: () => boolean; + private onSwipeDown: () => void; + private onSwipeUp: () => void; + private onCenterTap: () => void; + private keyupHandler: ((event: KeyboardEvent) => void) | null = null; + private wheelTimeoutId: ReturnType | null = null; + + constructor(options: EBookReaderOptions) { + this.container = options.container; + this.bookUrl = options.bookUrl; + this.documentId = options.documentId; + this.deviceId = options.deviceId; + this.deviceName = options.deviceName; + this.onReady = options.onReady; + this.onLoading = options.onLoading; + this.onError = options.onError; + this.onStats = options.onStats; + this.onToc = options.onToc; + this.onSaveProgress = options.onSaveProgress; + this.onCreateActivity = options.onCreateActivity; + this.isPaginationDisabled = options.isPaginationDisabled; + this.onSwipeDown = options.onSwipeDown; + this.onSwipeUp = options.onSwipeUp; + this.onCenterTap = options.onCenterTap; + + this.bookState = { + pages: 0, + percentage: 0, + progress: options.initialProgress ?? '', + progressElement: null, + readActivity: [], + words: 0, + pageStart: Date.now(), + }; + + this.loadSettings(); + this.readerSettings.theme = { + colorScheme: options.colorScheme, + fontFamily: options.fontFamily, + fontSize: options.fontSize, + }; + + this.onLoading(true); + this.book = ePub(this.bookUrl, { openAs: 'epub' }) as EpubBook; + this.rendition = this.book.renderTo(this.container, { + manager: 'default', + flow: 'paginated', + width: '100%', + height: '100%', + allowScriptedContent: true, + }); + + this.initCSP(); + this.initWakeLock(); + this.initThemes(); + this.initViewerListeners(); + this.initDocumentListeners(); + + this.book.ready.then(this.setupReader.bind(this)).catch(error => { + if (this.destroyed) { + return; + } + this.onError(error instanceof Error ? error.message : 'Unable to initialize reader'); + this.onLoading(false); + }); + } + + private loadSettings() { + this.readerSettings = { + theme: this.readerSettings.theme ?? {}, + }; + } + + private initWakeLock() { + this.noSleep = new NoSleep(); + document.addEventListener('wakelock', this.handleWakeLock); + } + + private handleWakeLock = () => { + if (!this.noSleep) { + return; + } + + if (this.wakeTimeoutId) { + clearTimeout(this.wakeTimeoutId); + } + this.wakeTimeoutId = setTimeout( + () => { + void this.noSleep?.disable(); + }, + 1000 * 60 * 10 + ); + + void this.noSleep.enable(); + }; + + private initThemes() { + THEMES.forEach(theme => this.rendition.themes.register(theme, THEME_FILE)); + + let themeLinkEl = document.querySelector('#themes') as HTMLLinkElement | null; + if (!themeLinkEl) { + themeLinkEl = document.createElement('link'); + themeLinkEl.id = 'themes'; + themeLinkEl.rel = 'stylesheet'; + themeLinkEl.href = THEME_FILE; + document.head.append(themeLinkEl); + } + + this.rendition.themes.default({ + '*': { + 'font-size': 'var(--editor-font-size) !important', + 'font-family': 'var(--editor-font-family) !important', + }, + }); + + this.rendition.hooks.content.register(() => { + this.setTheme(); + this.rendition.getContents().forEach(content => { + const existing = content.document.getElementById('reader-fonts'); + if (!existing) { + const nextLink = content.document.head.appendChild( + content.document.createElement('link') + ); + nextLink.id = 'reader-fonts'; + nextLink.rel = 'stylesheet'; + nextLink.href = FONT_FILE; + } + }); + }); + } + + private initCSP() { + const protocol = document.location.protocol; + const host = document.location.host; + const cspURL = `${protocol}//${host}`; + + this.book.spine.hooks.content.register(output => { + const cspWrapper = document.createElement('div'); + cspWrapper.innerHTML = ` + `; + const cspMeta = cspWrapper.children[0]; + if (cspMeta) { + output.head.append(cspMeta); + } + }); + } + + private initViewerListeners() { + const nextPage = this.nextPage.bind(this); + const prevPage = this.prevPage.bind(this); + + let touchStartX = 0; + let touchStartY = 0; + let touchEndX = 0; + let touchEndY = 0; + + const handleSwipeDown = () => { + this.resetWheelCooldown(); + this.onSwipeDown(); + }; + + const handleSwipeUp = () => { + this.resetWheelCooldown(); + this.onSwipeUp(); + }; + + const handleGesture = () => { + const drasticity = 50; + + if (touchEndY - drasticity > touchStartY) { + return handleSwipeDown(); + } + + if (touchEndY + drasticity < touchStartY) { + return handleSwipeUp(); + } + + if (!this.isPaginationDisabled() && touchEndX + drasticity < touchStartX) { + void nextPage(); + } + + if (!this.isPaginationDisabled() && touchEndX - drasticity > touchStartX) { + void prevPage(); + } + }; + + this.rendition.hooks.render.register((contents: EpubContents) => { + const renderDoc = contents.document; + + const wakeLockListener = () => { + renderDoc.dispatchEvent(new CustomEvent('wakelock')); + }; + renderDoc.addEventListener('click', wakeLockListener); + renderDoc.addEventListener('gesturechange', wakeLockListener); + renderDoc.addEventListener('touchstart', wakeLockListener); + + renderDoc.addEventListener('click', (event: MouseEvent) => { + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + const barPixels = windowHeight * 0.2; + const pagePixels = windowWidth * 0.2; + const top = barPixels; + const bottom = window.innerHeight - top; + const left = pagePixels; + const right = windowWidth - left; + const leftOffset = this.rendition.views().container.scrollLeft; + const yCoord = event.clientY; + const xCoord = event.clientX - leftOffset; + + if (yCoord < top) { + handleSwipeDown(); + } else if (yCoord > bottom) { + handleSwipeUp(); + } else if (!this.isPaginationDisabled() && xCoord < left) { + void prevPage(); + } else if (!this.isPaginationDisabled() && xCoord > right) { + void nextPage(); + } else { + this.onCenterTap(); + } + }); + + renderDoc.addEventListener('wheel', (event: WheelEvent) => { + if (this.wheelTimeoutId) { + return; + } + + if (event.deltaY > 25) { + handleSwipeUp(); + return; + } + if (event.deltaY < -25) { + handleSwipeDown(); + } + }); + + renderDoc.addEventListener( + 'touchstart', + (event: TouchEvent) => { + touchStartX = event.changedTouches[0]?.screenX ?? 0; + touchStartY = event.changedTouches[0]?.screenY ?? 0; + }, + false + ); + + renderDoc.addEventListener( + 'touchend', + (event: TouchEvent) => { + touchEndX = event.changedTouches[0]?.screenX ?? 0; + touchEndY = event.changedTouches[0]?.screenY ?? 0; + handleGesture(); + }, + false + ); + }); + } + + private resetWheelCooldown() { + if (this.wheelTimeoutId) { + clearTimeout(this.wheelTimeoutId); + this.wheelTimeoutId = null; + } + + this.wheelTimeoutId = setTimeout(() => { + this.wheelTimeoutId = null; + }, 400); + } + + private initDocumentListeners() { + const nextPage = this.nextPage.bind(this); + const prevPage = this.prevPage.bind(this); + + this.keyupHandler = (event: KeyboardEvent) => { + if ((event.keyCode || event.which) === 37) { + void prevPage(); + } + if ((event.keyCode || event.which) === 39) { + void nextPage(); + } + if ((event.keyCode || event.which) === 84) { + const currentTheme = this.readerSettings.theme?.colorScheme || 'tan'; + const currentThemeIdx = THEMES.indexOf(currentTheme); + const colorScheme = + THEMES.length === currentThemeIdx + 1 ? THEMES[0] : THEMES[currentThemeIdx + 1]; + if (colorScheme) { + this.setTheme({ colorScheme }); + } + } + }; + + document.addEventListener('keyup', this.keyupHandler, false); + } + + private async setupReader() { + this.bookState.words = await this.countWords(); + const { cfi } = await this.getCFIFromXPath(this.bookState.progress); + await this.setPosition(cfi); + const { element } = await this.getCFIFromXPath(this.bookState.progress); + this.bookState.progressElement = element ?? null; + this.highlightPositionMarker(); + const stats = await this.getBookStats(); + this.onStats(stats); + this.bookState.pageStart = Date.now(); + this.onToc(this.getParsedTOC()); + this.onLoading(false); + this.onReady(); + } + + private getParsedTOC(): ReaderTocItem[] { + if (!this.book.navigation?.toc) { + return []; + } + + return this.book.navigation.toc.reduce((agg: ReaderTocItem[], item) => { + const sectionTitle = item.label?.trim() ?? ''; + agg.push({ title: sectionTitle || 'Untitled', href: item.href }); + if (!item.subitems || item.subitems.length === 0) { + return agg; + } + + const allSubSections = item.subitems.map(subitem => { + let itemTitle = subitem.label?.trim() ?? 'Untitled'; + if (sectionTitle !== '') { + itemTitle = `${sectionTitle} - ${itemTitle}`; + } + return { title: itemTitle, href: subitem.href }; + }); + agg.push(...allSubSections); + return agg; + }, []); + } + + setTheme(newTheme?: { colorScheme?: ReaderColorScheme; fontFamily?: string; fontSize?: number }) { + this.readerSettings.theme = + typeof this.readerSettings.theme === 'object' && this.readerSettings.theme !== null + ? this.readerSettings.theme + : {}; + + Object.assign(this.readerSettings.theme, newTheme); + + const colorScheme = this.readerSettings.theme.colorScheme || 'tan'; + const fontFamily = this.readerSettings.theme.fontFamily || 'serif'; + const fontSize = this.readerSettings.theme.fontSize || 1; + + this.rendition.themes.select(colorScheme); + + const themeColorEl = document.querySelector("[name='theme-color']"); + const themeStyleSheet = (document.querySelector('#themes') as HTMLLinkElement | null)?.sheet; + const themeStyleRule = themeStyleSheet + ? Array.from(themeStyleSheet.cssRules).find( + item => (item as CSSStyleRule).selectorText === `.${colorScheme}` + ) + : null; + + if (!themeStyleRule) { + return; + } + + const backgroundColor = (themeStyleRule as CSSStyleRule).style.backgroundColor; + themeColorEl?.setAttribute('content', backgroundColor); + document.body.style.backgroundColor = backgroundColor; + + this.rendition.getContents().forEach(item => { + item.document.documentElement.style.setProperty('--editor-font-family', fontFamily); + item.document.documentElement.style.setProperty('--editor-font-size', `${fontSize}em`); + item.document.querySelectorAll('.highlight').forEach(element => { + Object.assign((element as HTMLElement).style, { + background: backgroundColor, + }); + }); + }); + } + + highlightPositionMarker() { + if (!this.bookState.progressElement) { + return; + } + + this.rendition.getContents().forEach(item => { + item.document.querySelectorAll('.highlight').forEach(element => { + element.removeAttribute('style'); + element.classList.remove('highlight'); + }); + }); + + const backgroundColor = getComputedStyle( + this.bookState.progressElement.ownerDocument.body + ).backgroundColor; + + Object.assign((this.bookState.progressElement as HTMLElement).style, { + background: backgroundColor, + filter: 'invert(0.2)', + }); + this.bookState.progressElement.classList.add('highlight'); + } + + async nextPage() { + try { + await this.createActivity(); + } catch (error) { + this.onError(error instanceof Error ? error.message : 'Unable to save reader activity'); + } + + await this.rendition.next(); + this.bookState.pageStart = Date.now(); + const stats = await this.getBookStats(); + this.onStats(stats); + void this.createProgress(); + } + + async prevPage() { + await this.rendition.prev(); + this.bookState.pageStart = Date.now(); + const stats = await this.getBookStats(); + this.onStats(stats); + void this.createProgress(); + } + + async displayHref(href: string) { + await this.rendition.display(href); + } + + async setPosition(cfi?: string) { + if (!cfi) { + return; + } + + await this.rendition.display(cfi); + await this.rendition.display(cfi); + await this.rendition.display(cfi); + this.highlightPositionMarker(); + } + + async applyThemeChange(newTheme: { + colorScheme?: ReaderColorScheme; + fontFamily?: string; + fontSize?: number; + }) { + const currentProgress = this.bookState.progress; + const { cfi } = await this.getCFIFromXPath(currentProgress); + this.setTheme(newTheme); + await this.setPosition(cfi); + const { element } = await this.getCFIFromXPath(currentProgress); + this.bookState.progressElement = element ?? null; + this.highlightPositionMarker(); + } + + async createActivity() { + const WPM_MAX = 2000; + const WPM_MIN = 100; + + const pageStart = this.bookState.pageStart; + let elapsedTime = Date.now() - pageStart; + const pageWords = await this.getVisibleWordCount(); + const currentWord = await this.getBookWordPosition(); + const percentRead = pageWords / this.bookState.words; + const pageWPM = pageWords / (elapsedTime / 60000); + + if (pageWPM >= WPM_MAX) { + return; + } + if (pageWPM < WPM_MIN) { + elapsedTime = (pageWords / WPM_MIN) * 60000; + } + + if (!Number.isFinite(percentRead) || percentRead <= 0 || this.bookState.words <= 0) { + return; + } + + const totalPages = Math.round(1 / percentRead); + if (!Number.isFinite(totalPages) || totalPages <= 0) { + return; + } + + const currentPage = Math.round((currentWord * totalPages) / this.bookState.words); + if (!Number.isFinite(currentPage) || currentPage < 0) { + return; + } + + const payload: CreateActivityRequest = { + device_id: this.deviceId, + device_name: this.deviceName, + activity: [ + { + document_id: this.documentId, + duration: Math.round(elapsedTime / 1000), + start_time: Math.round(pageStart / 1000), + page: currentPage, + pages: totalPages, + }, + ], + }; + + await this.onCreateActivity(payload); + } + + async createProgress() { + const currentCFI = await this.rendition.currentLocation(); + const { element, xpath } = await this.getXPathFromCFI(currentCFI.start.cfi); + const currentWord = await this.getBookWordPosition(); + this.bookState.progress = xpath ?? ''; + this.bookState.progressElement = element ?? null; + + const percentage = + this.bookState.words > 0 + ? Math.round((currentWord / this.bookState.words) * 100000) / 100000 + : 0; + this.bookState.percentage = Math.round(percentage * 10000) / 100; + + const payload: UpdateProgressRequest = { + document_id: this.documentId, + device_id: this.deviceId, + device_name: this.deviceName, + percentage, + progress: this.bookState.progress, + }; + + try { + await this.onSaveProgress(payload); + } catch (error) { + this.onError(error instanceof Error ? error.message : 'Unable to save reader progress'); + } + } + + sectionProgress() { + const visibleItems = this.rendition.manager?.visible?.() ?? []; + if (visibleItems.length === 0) { + return null; + } + const visibleSection = visibleItems[0]; + if (!visibleSection) { + return null; + } + + const totalBlocks = visibleSection.width() / visibleSection.layout.width; + const leftOffset = this.rendition.views().container.scrollLeft; + const sectionCurrentPage = Math.round(leftOffset / visibleSection.layout.width) + 1; + + return { + sectionPages: totalBlocks, + sectionCurrentPage, + }; + } + + async getBookStats(): Promise { + const currentProgress = this.sectionProgress(); + if (!currentProgress) { + return { + sectionPage: 0, + sectionTotalPages: 0, + chapterName: 'N/A', + percentage: this.bookState.percentage, + }; + } + + const currentLocation = await this.rendition.currentLocation(); + const currentWord = await this.getBookWordPosition(); + const currentTOC = this.book.navigation?.toc?.find( + item => item.href === currentLocation.start.href + ); + + return { + sectionPage: currentProgress.sectionCurrentPage, + sectionTotalPages: currentProgress.sectionPages, + chapterName: currentTOC ? currentTOC.label?.trim() || 'N/A' : 'N/A', + percentage: + this.bookState.words > 0 + ? Math.round((currentWord / this.bookState.words) * 10000) / 100 + : 0, + }; + } + + async getXPathFromCFI(cfi: string) { + const cfiBaseMatch = cfi.match(/\(([^!]+)/); + if (!cfiBaseMatch?.[1]) { + return {} as { xpath?: string; element?: Element | null }; + } + const startCFI = cfiBaseMatch[1]; + + const docFragmentIndex = + (this.book.spine.spineItems.find(item => item.cfiBase === startCFI)?.index ?? -1) + 1; + if (docFragmentIndex <= 0) { + return {} as { xpath?: string; element?: Element | null }; + } + + const basePos = `/body/DocFragment[${docFragmentIndex}]/body`; + const contents = this.rendition.getContents()[0]; + const currentNodeStart = contents?.range(cfi).startContainer; + if (!currentNodeStart) { + return {} as { xpath?: string; element?: Element | null }; + } + + let currentNode: Node | null = currentNodeStart; + const element = + currentNode.nodeType === Node.ELEMENT_NODE + ? (currentNode as Element) + : currentNode.parentElement; + + let allPos = ''; + while (currentNode && currentNode.nodeName !== 'BODY') { + let parentElement: Element | null = currentNode.parentElement; + if (!parentElement) { + break; + } + + if (currentNode.nodeType !== Node.ELEMENT_NODE) { + currentNode = parentElement; + continue; + } + + while (parentElement.nodeName === 'A' && parentElement.parentElement) { + parentElement = parentElement.parentElement; + } + + const currentElement = currentNode as Element; + const allDescendents = parentElement.querySelectorAll(currentElement.nodeName); + const relativeIndex = Array.from(allDescendents).indexOf(currentElement) + 1; + const nodePos = `${currentElement.nodeName.toLowerCase()}[${relativeIndex}]`; + currentNode = parentElement; + allPos = `/${nodePos}${allPos}`; + } + + return { xpath: `${basePos}${allPos}`, element }; + } + + async getCFIFromXPath(xpath?: string) { + if (!xpath) { + return {} as { cfi?: string; element?: Element | null }; + } + + const fragMatch = xpath.match(/^\/body\/DocFragment\[(\d+)\]/); + if (!fragMatch?.[1]) { + return {} as { cfi?: string; element?: Element | null }; + } + + const spinePosition = Number.parseInt(fragMatch[1], 10) - 1; + const sectionItem = this.book.spine.get(spinePosition); + await sectionItem.load(this.book.load.bind(this.book)); + + const renderedContent = this.rendition + .getContents() + .find(item => item.sectionIndex == spinePosition); + const docItem = renderedContent?.document || sectionItem.document; + + const namespaceURI = docItem.documentElement.namespaceURI; + let remainingXPath = xpath + .replace(fragMatch[0], '/html') + .replace(/\.(\d+)$/, '') + .replace(/\/text\(\)(\[\d+\])?$/, ''); + + const derivedSelectorElement = remainingXPath + .replace(/^\/html\/body/, 'body') + .split('/') + .reduce( + (element: ParentNode | null, item: string) => { + if (!element) { + return null; + } + + const indexMatch = item.match(/(\w+)\[(\d+)\]$/); + if (!indexMatch) { + return element.querySelector(item); + } + + const [, tag, rawIndex] = indexMatch; + if (!tag || !rawIndex) { + return null; + } + return element.querySelectorAll(tag)[Number.parseInt(rawIndex, 10) - 1] ?? null; + }, + docItem as ParentNode | null + ); + + if (namespaceURI) { + remainingXPath = remainingXPath.split('/').join('/ns:'); + } + + const docSearch = docItem.evaluate(remainingXPath, docItem, prefix => { + if (prefix === 'ns') { + return namespaceURI; + } + return null; + }); + + const xpathElement = docSearch.iterateNext(); + const element = xpathElement || derivedSelectorElement; + const isElementNode = Boolean(element && (element as Node).nodeType === Node.ELEMENT_NODE); + if (!isElementNode) { + return {} as { cfi?: string; element?: Element | null }; + } + + const resolvedElement = element as Element; + + let cfi = sectionItem.cfiFromElement(resolvedElement); + if (cfi.endsWith('!/)')) { + cfi = `${cfi.slice(0, -1)}0)`; + } + + return { cfi, element: resolvedElement }; + } + + async getVisibleWordCount() { + const visibleText = await this.getVisibleText(); + return visibleText.trim().split(/\s+/).length; + } + + async getBookWordPosition() { + const contents = this.rendition.getContents()[0]; + if (!contents) { + return 0; + } + + const spineItem = this.book.spine.get(contents.sectionIndex ?? 0); + const firstElement = spineItem.document.body.children[0]; + if (!firstElement) { + return 0; + } + + const firstCFI = spineItem.cfiFromElement(firstElement); + const currentLocation = await this.rendition.currentLocation(); + const cfiRange = this.getCFIRange(firstCFI, currentLocation.start.cfi); + const textRange = await this.book.getRange(cfiRange); + const chapterText = textRange.toString(); + const chapterWordPosition = chapterText.trim().split(/\s+/).length; + const preChapterWordPosition = this.book.spine.spineItems + .slice(0, contents.sectionIndex ?? 0) + .reduce((totalCount, item) => totalCount + (item.wordCount ?? 0), 0); + + return chapterWordPosition + preChapterWordPosition; + } + + async getVisibleText() { + this.rendition.manager?.visible?.()?.forEach(item => item.expand()); + const currentLocation = await this.rendition.currentLocation(); + const cfiRange = this.getCFIRange(currentLocation.start.cfi, currentLocation.end.cfi); + const textRange = await this.book.getRange(cfiRange); + return textRange.toString(); + } + + getCFIRange(a: string, b: string) { + const CFI = new (ePub as unknown as EpubWithCfiConstructor).CFI(); + const start = CFI.parse(a); + const end = CFI.parse(b); + const cfi: { + range: boolean; + base: unknown; + path: ParsedCfiPath; + start: ParsedCfiPath; + end: ParsedCfiPath; + } = { + range: true, + base: start.base, + path: { steps: [], terminal: null }, + start: start.path, + end: end.path, + }; + + const len = cfi.start.steps.length; + for (let i = 0; i < len; i += 1) { + if (CFI.equalStep(cfi.start.steps[i], cfi.end.steps[i])) { + if (i === len - 1) { + if (cfi.start.terminal === cfi.end.terminal) { + cfi.path.steps.push(cfi.start.steps[i]); + cfi.range = false; + } + } else { + cfi.path.steps.push(cfi.start.steps[i]); + } + } else { + break; + } + } + + cfi.start.steps = cfi.start.steps.slice(cfi.path.steps.length); + cfi.end.steps = cfi.end.steps.slice(cfi.path.steps.length); + + return `epubcfi(${CFI.segmentString(cfi.base)}!${CFI.segmentString(cfi.path)},${CFI.segmentString(cfi.start)},${CFI.segmentString(cfi.end)})`; + } + + async countWords() { + const spineWC = await Promise.all( + this.book.spine.spineItems.map(async item => { + const newDoc = await item.load(this.book.load.bind(this.book)); + const spineWords = ((newDoc as unknown as HTMLElement).innerText || '') + .trim() + .split(/\s+/).length; + item.wordCount = spineWords; + return spineWords; + }) + ); + + return spineWC.reduce((totalCount, itemCount) => totalCount + itemCount, 0); + } + + destroy() { + this.destroyed = true; + if (this.keyupHandler) { + document.removeEventListener('keyup', this.keyupHandler, false); + } + document.removeEventListener('wakelock', this.handleWakeLock); + if (this.wakeTimeoutId) { + clearTimeout(this.wakeTimeoutId); + } + if (this.wheelTimeoutId) { + clearTimeout(this.wheelTimeoutId); + } + void this.noSleep?.disable(); + this.rendition.destroy?.(); + this.book.destroy?.(); + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..f6cb638 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ToastProvider } from './components/ToastContext'; +import { ThemeProvider, initializeThemeMode } from './theme/ThemeProvider'; +import App from './App'; +import './index.css'; + +initializeThemeMode(); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, + retry: 1, + }, + mutations: { + retry: 0, + }, + }, +}); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + + + + +); diff --git a/frontend/src/pages/ActivityPage.tsx b/frontend/src/pages/ActivityPage.tsx new file mode 100644 index 0000000..0699d04 --- /dev/null +++ b/frontend/src/pages/ActivityPage.tsx @@ -0,0 +1,39 @@ +import { Link } from 'react-router-dom'; +import { useGetActivity } from '../generated/anthoLumeAPIV1'; +import type { Activity } from '../generated/model'; +import { Table, type Column } from '../components/Table'; +import { formatDuration } from '../utils/formatters'; + +export default function ActivityPage() { + const { data, isLoading } = useGetActivity({ offset: 0, limit: 100 }); + const activities = data?.status === 200 ? data.data.activities : []; + + const columns: Column[] = [ + { + key: 'document_id' as const, + header: 'Document', + render: (_value, row) => ( + + {row.author || 'Unknown'} - {row.title || 'Unknown'} + + ), + }, + { + key: 'start_time' as const, + header: 'Time', + render: value => String(value || 'N/A'), + }, + { + key: 'duration' as const, + header: 'Duration', + render: value => formatDuration(typeof value === 'number' ? value : 0), + }, + { + key: 'end_percentage' as const, + header: 'Percent', + render: value => (typeof value === 'number' ? `${value}%` : '0%'), + }, + ]; + + return ; +} diff --git a/frontend/src/pages/AdminImportPage.tsx b/frontend/src/pages/AdminImportPage.tsx new file mode 100644 index 0000000..0427568 --- /dev/null +++ b/frontend/src/pages/AdminImportPage.tsx @@ -0,0 +1,173 @@ +import { useState } from 'react'; +import { useGetImportDirectory, usePostImport } from '../generated/anthoLumeAPIV1'; +import type { DirectoryItem, DirectoryListResponse } from '../generated/model'; +import { getErrorMessage } from '../utils/errors'; +import { Button } from '../components/Button'; +import { FolderOpenIcon } from '../icons'; +import { useToasts } from '../components/ToastContext'; + +export default function AdminImportPage() { + const [currentPath, setCurrentPath] = useState(''); + const [selectedDirectory, setSelectedDirectory] = useState(''); + const [importType, setImportType] = useState<'DIRECT' | 'COPY'>('DIRECT'); + const { showInfo, showError } = useToasts(); + + const { data: directoryData, isLoading } = useGetImportDirectory( + currentPath ? { directory: currentPath } : {} + ); + + const postImport = usePostImport(); + + const directoryResponse = + directoryData?.status === 200 ? (directoryData.data as DirectoryListResponse) : null; + const directories = directoryResponse?.items ?? []; + const currentPathDisplay = directoryResponse?.current_path ?? currentPath ?? '/data'; + + const handleSelectDirectory = (directory: string) => { + setSelectedDirectory(`${currentPath}/${directory}`); + }; + + const handleNavigateUp = () => { + if (currentPathDisplay !== '/') { + const parts = currentPathDisplay.split('/'); + parts.pop(); + setCurrentPath(parts.join('/') || ''); + } + }; + + const handleImport = () => { + if (!selectedDirectory) return; + + postImport.mutate( + { + data: { + directory: selectedDirectory, + type: importType, + }, + }, + { + onSuccess: _response => { + showInfo('Import completed successfully'); + setTimeout(() => { + window.location.href = '/admin/import-results'; + }, 1500); + }, + onError: error => { + showError('Import failed: ' + getErrorMessage(error)); + }, + } + ); + }; + + const handleCancel = () => { + setSelectedDirectory(''); + }; + + if (isLoading && !currentPath) { + return
Loading...
; + } + + if (selectedDirectory) { + return ( +
+
+
+

Selected Import Directory

+
+
+
+ +

{selectedDirectory}

+
+
+
+ setImportType('DIRECT')} + /> + +
+
+ setImportType('COPY')} + /> + +
+
+
+
+ + +
+ +
+
+
+ ); + } + + return ( +
+
+
+ + + + + + + + {currentPath !== '/' && ( + + + + + )} + {directories.length === 0 ? ( + + + + ) : ( + directories.map((item: DirectoryItem) => ( + + + + + )) + )} + +
+ {currentPath} +
+ +
+ No Folders +
+ + + +
+
+ + ); +} diff --git a/frontend/src/pages/AdminImportResultsPage.tsx b/frontend/src/pages/AdminImportResultsPage.tsx new file mode 100644 index 0000000..43e27ce --- /dev/null +++ b/frontend/src/pages/AdminImportResultsPage.tsx @@ -0,0 +1,70 @@ +import { useGetImportResults } from '../generated/anthoLumeAPIV1'; +import type { ImportResult, ImportResultsResponse } from '../generated/model'; +import { Link } from 'react-router-dom'; + +export default function AdminImportResultsPage() { + const { data: resultsData, isLoading } = useGetImportResults(); + const results = + resultsData?.status === 200 ? (resultsData.data as ImportResultsResponse).results || [] : []; + + if (isLoading) { + return
Loading...
; + } + + return ( +
+
+ + + + + + + + + + {results.length === 0 ? ( + + + + ) : ( + results.map((result: ImportResult, index: number) => ( + + + + + + )) + )} + +
+ Document + + Status + + Error +
+ No Results +
+ Name: + {result.id ? ( + + {result.name} + + ) : ( + N/A + )} + File: + {result.path} + +

{result.status}

+
+

{result.error || ''}

+
+
+
+ ); +} diff --git a/frontend/src/pages/AdminLogsPage.tsx b/frontend/src/pages/AdminLogsPage.tsx new file mode 100644 index 0000000..b3a3a59 --- /dev/null +++ b/frontend/src/pages/AdminLogsPage.tsx @@ -0,0 +1,69 @@ +import { useState, useEffect, FormEvent } from 'react'; +import { useGetLogs } from '../generated/anthoLumeAPIV1'; +import type { LogsResponse } from '../generated/model'; +import { Button } from '../components/Button'; +import { LoadingState } from '../components'; +import { useDebounce } from '../hooks/useDebounce'; +import { Search2Icon } from '../icons'; + +export default function AdminLogsPage() { + const [filter, setFilter] = useState(''); + const [activeFilter, setActiveFilter] = useState(''); + const debouncedFilter = useDebounce(filter, 300); + + useEffect(() => { + setActiveFilter(debouncedFilter); + }, [debouncedFilter]); + + const { data: logsData, isLoading } = useGetLogs(activeFilter ? { filter: activeFilter } : {}); + + const logs = logsData?.status === 200 ? ((logsData.data as LogsResponse).logs ?? []) : []; + + const handleFilterSubmit = (e: FormEvent) => { + e.preventDefault(); + setActiveFilter(filter); + }; + + return ( +
+
+
+
+
+ + + + setFilter(e.target.value)} + className="w-full flex-1 appearance-none rounded-none border border-border bg-surface p-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600" + placeholder="JQ Filter" + /> +
+
+
+ +
+
+
+ +
+ {isLoading ? ( + + ) : ( + logs.map((log, index) => ( + + {typeof log === 'string' ? log : JSON.stringify(log)} + + )) + )} +
+
+ ); +} diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx new file mode 100644 index 0000000..fccc11a --- /dev/null +++ b/frontend/src/pages/AdminPage.tsx @@ -0,0 +1,215 @@ +import { useState, FormEvent } from 'react'; +import { useGetAdmin, usePostAdminAction } from '../generated/anthoLumeAPIV1'; +import { Button } from '../components/Button'; +import { useToasts } from '../components/ToastContext'; +import { getErrorMessage } from '../utils/errors'; + +interface BackupTypes { + covers: boolean; + documents: boolean; +} + +export default function AdminPage() { + const { isLoading } = useGetAdmin(); + const postAdminAction = usePostAdminAction(); + const { showInfo, showError, removeToast } = useToasts(); + + const [backupTypes, setBackupTypes] = useState({ + covers: false, + documents: false, + }); + const [restoreFile, setRestoreFile] = useState(null); + + const handleBackupSubmit = async (e: FormEvent) => { + e.preventDefault(); + const backupTypesList: string[] = []; + if (backupTypes.covers) backupTypesList.push('COVERS'); + if (backupTypes.documents) backupTypesList.push('DOCUMENTS'); + + try { + const formData = new FormData(); + formData.append('action', 'BACKUP'); + backupTypesList.forEach(value => formData.append('backup_types', value)); + + const response = await fetch('/api/v1/admin', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error('Backup failed: ' + response.statusText); + } + + const filename = `AnthoLumeBackup_${new Date().toISOString().replace(/[:.]/g, '')}.zip`; + + if ('showSaveFilePicker' in window && typeof window.showSaveFilePicker === 'function') { + try { + const handle = await window.showSaveFilePicker({ + suggestedName: filename, + types: [{ description: 'ZIP Archive', accept: { 'application/zip': ['.zip'] } }], + }); + + const writable = await handle.createWritable(); + const reader = response.body?.getReader(); + if (!reader) throw new Error('Unable to read response'); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + await writable.write(value); + } + + await writable.close(); + showInfo('Backup completed successfully'); + } catch (err) { + if ((err as Error).name !== 'AbortError') { + showError('Backup failed: ' + (err as Error).message); + } + } + } else { + showError( + 'Your browser does not support large file downloads. Please use Chrome, Edge, or Safari.' + ); + } + } catch (error) { + showError('Backup failed: ' + getErrorMessage(error)); + } + }; + + const handleRestoreSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!restoreFile) return; + + const startedToastId = showInfo('Restore started', 0); + + try { + const response = await postAdminAction.mutateAsync({ + data: { + action: 'RESTORE', + restore_file: restoreFile, + }, + }); + + removeToast(startedToastId); + + if (response.status >= 200 && response.status < 300) { + showInfo('Restore completed successfully'); + return; + } + + showError('Restore failed: ' + getErrorMessage(response.data)); + } catch (error) { + removeToast(startedToastId); + showError('Restore failed: ' + getErrorMessage(error)); + } + }; + + const handleMetadataMatch = () => { + postAdminAction.mutate( + { data: { action: 'METADATA_MATCH' } }, + { + onSuccess: () => showInfo('Metadata matching started'), + onError: error => showError('Metadata matching failed: ' + getErrorMessage(error)), + } + ); + }; + + const handleCacheTables = () => { + postAdminAction.mutate( + { data: { action: 'CACHE_TABLES' } }, + { + onSuccess: () => showInfo('Cache tables started'), + onError: error => showError('Cache tables failed: ' + getErrorMessage(error)), + } + ); + }; + + if (isLoading) { + return
Loading...
; + } + + return ( +
+
+

Backup & Restore

+
+
+
+
+ setBackupTypes({ ...backupTypes, covers: e.target.checked })} + /> + +
+
+ setBackupTypes({ ...backupTypes, documents: e.target.checked })} + /> + +
+
+
+ +
+
+ +
+
+ setRestoreFile(e.target.files?.[0] || null)} + className="w-full" + /> +
+
+ +
+
+
+
+ +
+

Tasks

+ + + + + + + + + + + +
+

Metadata Matching

+
+
+ +
+
+

Cache Tables

+
+
+ +
+
+
+
+ ); +} diff --git a/frontend/src/pages/AdminUsersPage.tsx b/frontend/src/pages/AdminUsersPage.tsx new file mode 100644 index 0000000..6d07f49 --- /dev/null +++ b/frontend/src/pages/AdminUsersPage.tsx @@ -0,0 +1,239 @@ +import { useState, FormEvent } from 'react'; +import { useGetUsers, useUpdateUser } from '../generated/anthoLumeAPIV1'; +import type { User, UsersResponse } from '../generated/model'; +import { AddIcon, DeleteIcon } from '../icons'; +import { useToasts } from '../components/ToastContext'; +import { getErrorMessage } from '../utils/errors'; + +export default function AdminUsersPage() { + const { data: usersData, isLoading, refetch } = useGetUsers({}); + const updateUser = useUpdateUser(); + const { showInfo, showError } = useToasts(); + + const [showAddForm, setShowAddForm] = useState(false); + const [newUsername, setNewUsername] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [newIsAdmin, setNewIsAdmin] = useState(false); + + const users = usersData?.status === 200 ? ((usersData.data as UsersResponse).users ?? []) : []; + + const handleCreateUser = (e: FormEvent) => { + e.preventDefault(); + if (!newUsername || !newPassword) return; + + updateUser.mutate( + { + data: { + operation: 'CREATE', + user: newUsername, + password: newPassword, + is_admin: newIsAdmin, + }, + }, + { + onSuccess: response => { + if (response.status < 200 || response.status >= 300) { + showError('Failed to create user: ' + getErrorMessage(response.data)); + return; + } + + showInfo('User created successfully'); + setShowAddForm(false); + setNewUsername(''); + setNewPassword(''); + setNewIsAdmin(false); + refetch(); + }, + onError: error => showError('Failed to create user: ' + getErrorMessage(error)), + } + ); + }; + + const handleDeleteUser = (userId: string) => { + updateUser.mutate( + { + data: { operation: 'DELETE', user: userId }, + }, + { + onSuccess: response => { + if (response.status < 200 || response.status >= 300) { + showError('Failed to delete user: ' + getErrorMessage(response.data)); + return; + } + + showInfo('User deleted successfully'); + refetch(); + }, + onError: error => showError('Failed to delete user: ' + getErrorMessage(error)), + } + ); + }; + + const handleUpdatePassword = (userId: string, password: string) => { + if (!password) return; + + updateUser.mutate( + { + data: { operation: 'UPDATE', user: userId, password }, + }, + { + onSuccess: response => { + if (response.status < 200 || response.status >= 300) { + showError('Failed to update password: ' + getErrorMessage(response.data)); + return; + } + + showInfo('Password updated successfully'); + refetch(); + }, + onError: error => showError('Failed to update password: ' + getErrorMessage(error)), + } + ); + }; + + const handleToggleAdmin = (userId: string, isAdmin: boolean) => { + updateUser.mutate( + { + data: { operation: 'UPDATE', user: userId, is_admin: isAdmin }, + }, + { + onSuccess: response => { + if (response.status < 200 || response.status >= 300) { + showError('Failed to update admin status: ' + getErrorMessage(response.data)); + return; + } + + showInfo(`User permissions updated to ${isAdmin ? 'admin' : 'user'}`); + refetch(); + }, + onError: error => showError('Failed to update admin status: ' + getErrorMessage(error)), + } + ); + }; + + if (isLoading) { + return
Loading...
; + } + + return ( +
+ {showAddForm && ( +
+
+ setNewUsername(e.target.value)} + placeholder="Username" + className="bg-surface p-2 text-content" + /> + setNewPassword(e.target.value)} + placeholder="Password" + className="bg-surface p-2 text-content" + /> +
+ setNewIsAdmin(e.target.checked)} + /> + +
+ +
+
+ )} + +
+ + + + + + + + + + + + {users.length === 0 ? ( + + + + ) : ( + users.map((user: User) => ( + + + + + + + + )) + )} + +
+ + UserPassword + Permissions + + Created +
+ No Results +
+ + +

{user.id}

+
+ + + + + +

{user.created_at}

+
+
+
+ ); +} diff --git a/frontend/src/pages/ComponentDemoPage.tsx b/frontend/src/pages/ComponentDemoPage.tsx new file mode 100644 index 0000000..0281249 --- /dev/null +++ b/frontend/src/pages/ComponentDemoPage.tsx @@ -0,0 +1,156 @@ +import { useState } from 'react'; +import { useToasts } from '../components/ToastContext'; +import { + Skeleton, + SkeletonText, + SkeletonAvatar, + SkeletonCard, + SkeletonTable, + SkeletonButton, + PageLoader, + InlineLoader, +} from '../components/Skeleton'; + +export default function ComponentDemoPage() { + const { showInfo, showWarning, showError, showToast } = useToasts(); + const [isLoading, setIsLoading] = useState(false); + + const handleDemoClick = () => { + setIsLoading(true); + showInfo('Starting demo operation...'); + + setTimeout(() => { + setIsLoading(false); + showInfo('Demo operation completed successfully!'); + }, 2000); + }; + + const handleErrorClick = () => { + showError('This is a sample error message'); + }; + + const handleWarningClick = () => { + showWarning('This is a sample warning message', 10000); + }; + + const handleCustomToast = () => { + showToast('Custom toast message', 'info', 3000); + }; + + return ( +
+

UI Components Demo

+ +
+

Toast Notifications

+
+ + + + +
+
+ +
+

Skeleton Loading Components

+ +
+
+

Basic Skeletons

+
+ + + +
+ + +
+
+
+ +
+

Skeleton Text

+ + +
+ +
+

Skeleton Avatar

+
+ + + + +
+
+ +
+

Skeleton Button

+
+ + +
+
+
+
+ +
+

Skeleton Cards

+
+ + + +
+
+ +
+

Skeleton Table

+ +
+ +
+

Page Loader

+ +
+ +
+

Inline Loader

+
+
+ +

Small

+
+
+ +

Medium

+
+
+ +

Large

+
+
+
+
+ ); +} diff --git a/frontend/src/pages/DocumentPage.tsx b/frontend/src/pages/DocumentPage.tsx new file mode 100644 index 0000000..fffa5c3 --- /dev/null +++ b/frontend/src/pages/DocumentPage.tsx @@ -0,0 +1,455 @@ +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; +import { + useGetDocument, + useEditDocument, + getGetDocumentQueryKey, +} from '../generated/anthoLumeAPIV1'; +import { Document } from '../generated/model/document'; +import { formatDuration } from '../utils/formatters'; +import { + DeleteIcon, + ActivityIcon, + SearchIcon, + DownloadIcon, + EditIcon, + InfoIcon, + CloseIcon, + CheckIcon, +} from '../icons'; +import { Field, FieldLabel, FieldValue, FieldActions } from '../components'; + +const iconButtonClassName = 'cursor-pointer text-content-muted hover:text-content'; +const popupClassName = 'rounded bg-surface-strong p-3 text-content shadow-lg transition-all duration-200'; +const popupInputClassName = 'rounded bg-surface p-2 text-content'; +const editInputClassName = + 'w-full rounded border border-secondary-200 bg-secondary-50 p-2 text-lg font-medium text-content focus:outline-none focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500'; + +export default function DocumentPage() { + const { id } = useParams<{ id: string }>(); + const queryClient = useQueryClient(); + const { data: docData, isLoading: docLoading } = useGetDocument(id || ''); + const editMutation = useEditDocument(); + + const [showEditCover, setShowEditCover] = useState(false); + const [showDelete, setShowDelete] = useState(false); + const [showIdentify, setShowIdentify] = useState(false); + const [isEditingTitle, setIsEditingTitle] = useState(false); + const [isEditingAuthor, setIsEditingAuthor] = useState(false); + const [isEditingDescription, setIsEditingDescription] = useState(false); + const [showTimeReadInfo, setShowTimeReadInfo] = useState(false); + + const [editTitle, setEditTitle] = useState(''); + const [editAuthor, setEditAuthor] = useState(''); + const [editDescription, setEditDescription] = useState(''); + + if (docLoading) { + return
Loading...
; + } + + if (!docData || docData.status !== 200) { + return
Document not found
; + } + + const document = docData.data.document as Document; + + if (!document) { + return
Document not found
; + } + + const percentage = document.percentage ?? 0; + const secondsPerPercent = document.seconds_per_percent || 0; + const totalTimeLeftSeconds = Math.round((100 - percentage) * secondsPerPercent); + + const startEditing = (field: 'title' | 'author' | 'description') => { + if (field === 'title') setEditTitle(document.title); + if (field === 'author') setEditAuthor(document.author); + if (field === 'description') setEditDescription(document.description || ''); + }; + + const saveTitle = () => { + editMutation.mutate( + { id: document.id, data: { title: editTitle } }, + { + onSuccess: response => { + setIsEditingTitle(false); + queryClient.setQueryData(getGetDocumentQueryKey(document.id), response); + }, + onError: () => setIsEditingTitle(false), + } + ); + }; + + const saveAuthor = () => { + editMutation.mutate( + { id: document.id, data: { author: editAuthor } }, + { + onSuccess: response => { + setIsEditingAuthor(false); + queryClient.setQueryData(getGetDocumentQueryKey(document.id), response); + }, + onError: () => setIsEditingAuthor(false), + } + ); + }; + + const saveDescription = () => { + editMutation.mutate( + { id: document.id, data: { description: editDescription } }, + { + onSuccess: response => { + setIsEditingDescription(false); + queryClient.setQueryData(getGetDocumentQueryKey(document.id), response); + }, + onError: () => setIsEditingDescription(false), + } + ); + }; + + return ( +
+
+
+ + + {document.filepath && ( + + Read + + )} + +
+
+
+

ISBN-10:

+

{document.isbn10 || 'N/A'}

+
+
+

ISBN-13:

+

{document.isbn13 || 'N/A'}

+
+
+ +
+ setShowEditCover(e.target.checked)} + /> +
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+ + + + + +
+ +
+
+ + + + +
+
+
+ + {document.filepath ? ( + + + + ) : ( + + + + )} +
+
+
+ +
+ + Title + + {isEditingTitle ? ( +
+ + +
+ ) : ( + + )} +
+ + } + > + {isEditingTitle ? ( +
+ setEditTitle(e.target.value)} className={editInputClassName} /> +
+ ) : ( + {document.title} + )} +
+ + + Author + + {isEditingAuthor ? ( + <> + + + + ) : ( + + )} + + + } + > + {isEditingAuthor ? ( +
+ setEditAuthor(e.target.value)} className={editInputClassName} /> +
+ ) : ( + {document.author} + )} +
+ + + Time Read + +
+
+

Seconds / Percent

+

{secondsPerPercent !== 0 ? secondsPerPercent : 'N/A'}

+
+
+

Words / Minute

+

{document.wpm && document.wpm > 0 ? document.wpm : 'N/A'}

+
+
+

Est. Time Left

+

+ {totalTimeLeftSeconds > 0 ? formatDuration(totalTimeLeftSeconds) : 'N/A'} +

+
+
+ + } + > + + {document.total_time_seconds && document.total_time_seconds > 0 + ? formatDuration(document.total_time_seconds) + : 'N/A'} + +
+ + Progress}> + {`${percentage.toFixed(2)}%`} + +
+ + + Description + + {isEditingDescription ? ( + <> + + + + ) : ( + + )} + + + } + > + {isEditingDescription ? ( +
+