diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..65a4769 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# Agent Context Hints + +## Architecture Context +- **Backend**: Go with Gin router (legacy), SQLC for database queries, currently migrating to V1 API (oapi-codegen) +- **Frontend**: React with Vite, currently migrating from Go templates (using the V1 API) +- **API**: OpenAPI 3.0 spec, generates Go server (oapi-codegen) and TS client (orval) + +## Data Flow (CRITICAL for migrations) +1. Database schema → SQL queries (`database/query.sql`, `database/query.sql.go`) +2. SQLC models → API handlers (`api/v1/*.go`) +3. Go templates show **intended UI** structure (`templates/pages/*.tmpl`) +4. API spec defines actual API contract (`api/v1/openapi.yaml`) +5. Generated TS client → React components + +## When Migrating from Go Templates +- Check template AND database query results (Go templates may show fields API doesn't return) +- Template columns often map to: document_id, title, author, start_time, duration, start/end_percentage +- Go template rendering: `{{ template "component/table" }}` with "Columns" and "Keys" + +## API Regeneration Commands +- Go backend: `go generate ./api/v1/generate.go` +- TS client: `cd frontend && npm run generate:api` + +## Key Files +- Database queries: `database/query.sql` → SQLc Query shows actual fields returned +- SQLC models: `database/query.sql.go` → SQLc Generated Go struct definitions +- Go templates: `templates/pages/*.tmpl` → Legacy UI reference +- API spec: `api/v1/openapi.yaml` → contract definition +- Generated TS types: `frontend/src/generated/model/*.ts` + +## Common Gotchas +- API implementation may not map all fields from DB query (check `api/v1/activity.go` mapping) +- `start_time` is `interface{}` in Go models, needs type assertion +- Go templates use `LOCAL_TIME()` SQL function for timezone-aware display diff --git a/antholume b/antholume new file mode 100755 index 0000000..c221c86 Binary files /dev/null and b/antholume differ diff --git a/api/v1/activity.go b/api/v1/activity.go index 90b46ae..a8123d6 100644 --- a/api/v1/activity.go +++ b/api/v1/activity.go @@ -2,8 +2,6 @@ package v1 import ( "context" - "strconv" - "time" "reichard.io/antholume/database" ) @@ -48,12 +46,24 @@ func (s *Server) GetActivity(ctx context.Context, request GetActivityRequestObje 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{ - ActivityType: a.DeviceID, - DocumentId: a.DocumentID, - Id: strconv.Itoa(i), - Timestamp: time.Now(), - UserId: auth.UserName, + 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), } } diff --git a/api/v1/admin.go b/api/v1/admin.go new file mode 100644 index 0000000..d8c633c --- /dev/null +++ b/api/v1/admin.go @@ -0,0 +1,140 @@ +package v1 + +import ( + "context" + "time" +) + +// 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 database info from the main API + // This is a placeholder - you'll need to implement this in the main API or database + // For now, return empty data + response := GetAdmin200JSONResponse{ + DatabaseInfo: &DatabaseInfo{ + DocumentsSize: 0, + ActivitySize: 0, + ProgressSize: 0, + DevicesSize: 0, + }, + } + 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 + } + + // TODO: Implement admin actions (backup, restore, etc.) + // For now, this is a placeholder + return PostAdminAction200ApplicationoctetStreamResponse{}, 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 { + createdAt, _ := time.Parse("2006-01-02T15:04:05", user.CreatedAt) + apiUsers[i] = User{ + Id: user.ID, + Admin: user.Admin, + CreatedAt: 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 + } + + // TODO: Implement user creation, update, deletion + // For now, this is a placeholder + return UpdateUser200JSONResponse{ + Users: &[]User{}, + }, 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 + } + + // TODO: Implement directory listing + // For now, this is a placeholder + return GetImportDirectory200JSONResponse{ + CurrentPath: ptrOf("/data"), + Items: &[]DirectoryItem{}, + }, 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 + } + + // TODO: Implement import functionality + // For now, this is a placeholder + return PostImport200JSONResponse{ + Results: &[]ImportResult{}, + }, nil +} + +// 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 + } + + // TODO: Implement import results retrieval + // For now, this is a placeholder + 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 + } + + // TODO: Implement log retrieval + // For now, this is a placeholder + return GetLogs200JSONResponse{ + Logs: &[]string{}, + Filter: request.Params.Filter, + }, nil +} diff --git a/api/v1/api.gen.go b/api/v1/api.gen.go index bb51584..17ac16d 100644 --- a/api/v1/api.gen.go +++ b/api/v1/api.gen.go @@ -9,6 +9,7 @@ import ( "context" "encoding/json" "fmt" + "io" "mime/multipart" "net/http" "time" @@ -22,6 +23,108 @@ 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 PostAdminActionFormdataBodyAction. +const ( + BACKUP PostAdminActionFormdataBodyAction = "BACKUP" + CACHETABLES PostAdminActionFormdataBodyAction = "CACHE_TABLES" + METADATAMATCH PostAdminActionFormdataBodyAction = "METADATA_MATCH" + RESTORE PostAdminActionFormdataBodyAction = "RESTORE" +) + +// Valid indicates whether the value is a known member of the PostAdminActionFormdataBodyAction enum. +func (e PostAdminActionFormdataBodyAction) 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" @@ -42,11 +145,15 @@ func (e GetSearchParamsSource) Valid() bool { // Activity defines model for Activity. type Activity struct { - ActivityType string `json:"activity_type"` - DocumentId string `json:"document_id"` - Id string `json:"id"` - Timestamp time.Time `json:"timestamp"` - UserId string `json:"user_id"` + 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. @@ -55,6 +162,9 @@ type ActivityResponse struct { User UserData `json:"user"` } +// BackupType defines model for BackupType. +type BackupType string + // DatabaseInfo defines model for DatabaseInfo. type DatabaseInfo struct { ActivitySize int64 `json:"activity_size"` @@ -71,6 +181,18 @@ type Device struct { 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"` @@ -132,6 +254,26 @@ type HomeResponse struct { 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 + // LeaderboardData defines model for LeaderboardData. type LeaderboardData struct { All []LeaderboardEntry `json:"all"` @@ -146,6 +288,9 @@ type LeaderboardEntry struct { Value int64 `json:"value"` } +// LogEntry defines model for LogEntry. +type LogEntry = string + // LoginRequest defines model for LoginRequest. type LoginRequest struct { Password string `json:"password"` @@ -158,6 +303,15 @@ type LoginResponse struct { Username string `json:"username"` } +// LogsResponse defines model for LogsResponse. +type LogsResponse struct { + Filter *string `json:"filter,omitempty"` + Logs *[]LogEntry `json:"logs,omitempty"` +} + +// OperationType defines model for OperationType. +type OperationType string + // Progress defines model for Progress. type Progress struct { Author *string `json:"author,omitempty"` @@ -218,6 +372,13 @@ type StreaksResponse struct { User UserData `json:"user"` } +// 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"` @@ -243,6 +404,11 @@ type UserStreak struct { Window string `json:"window"` } +// UsersResponse defines model for UsersResponse. +type UsersResponse struct { + Users *[]User `json:"users,omitempty"` +} + // WordCount defines model for WordCount. type WordCount struct { Count int64 `json:"count"` @@ -257,6 +423,41 @@ type GetActivityParams struct { Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` } +// PostAdminActionFormdataBody defines parameters for PostAdminAction. +type PostAdminActionFormdataBody struct { + Action PostAdminActionFormdataBodyAction `form:"action" json:"action"` + BackupTypes *[]BackupType `form:"backup_types,omitempty" json:"backup_types,omitempty"` + RestoreFile *openapi_types.File `form:"restore_file,omitempty" json:"restore_file,omitempty"` +} + +// PostAdminActionFormdataBodyAction defines parameters for PostAdminAction. +type PostAdminActionFormdataBodyAction 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"` +} + +// 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"` @@ -293,6 +494,15 @@ type PostSearchFormdataBody struct { Title string `form:"title" json:"title"` } +// PostAdminActionFormdataRequestBody defines body for PostAdminAction for application/x-www-form-urlencoded ContentType. +type PostAdminActionFormdataRequestBody PostAdminActionFormdataBody + +// 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 @@ -307,6 +517,30 @@ type ServerInterface interface { // Get activity data // (GET /activity) GetActivity(w http.ResponseWriter, r *http.Request, params GetActivityParams) + // 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) @@ -420,6 +654,200 @@ func (siw *ServerInterfaceWrapper) GetActivity(w http.ResponseWriter, r *http.Re 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 + } + + 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) { @@ -950,6 +1378,14 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H } m.HandleFunc("GET "+options.BaseURL+"/activity", wrapper.GetActivity) + 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) @@ -1004,6 +1440,313 @@ func (response GetActivity500JSONResponse) VisitGetActivityResponse(w http.Respo 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 *PostAdminActionFormdataRequestBody +} + +type PostAdminActionResponseObject interface { + VisitPostAdminActionResponse(w http.ResponseWriter) error +} + +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 } @@ -1552,6 +2295,30 @@ type StrictServerInterface interface { // Get activity data // (GET /activity) GetActivity(ctx context.Context, request GetActivityRequestObject) (GetActivityResponseObject, 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) @@ -1654,6 +2421,235 @@ func (sh *strictHandler) GetActivity(w http.ResponseWriter, r *http.Request, par } } +// 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 err := r.ParseForm(); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode formdata: %w", err)) + return + } + var body PostAdminActionFormdataRequestBody + 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.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 diff --git a/api/v1/openapi.yaml b/api/v1/openapi.yaml index 0d2a94e..74d030d 100644 --- a/api/v1/openapi.yaml +++ b/api/v1/openapi.yaml @@ -91,23 +91,36 @@ components: Activity: type: object properties: - id: - type: string - user_id: - type: string document_id: type: string - activity_type: + device_id: type: string - timestamp: + start_time: type: string - format: date-time + 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: - - id - - user_id - document_id - - activity_type - - timestamp + - device_id + - start_time + - duration + - start_percentage + - end_percentage + - read_percentage SearchItem: type: object @@ -482,6 +495,95 @@ components: - user_statistics - user + 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 + securitySchemes: BearerAuth: type: http @@ -1057,4 +1159,307 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' \ No newline at end of file + $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: + application/x-www-form-urlencoded: + 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/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 + 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/frontend/src/Routes.tsx b/frontend/src/Routes.tsx index 5ad3f4e..c9b1ec5 100644 --- a/frontend/src/Routes.tsx +++ b/frontend/src/Routes.tsx @@ -10,6 +10,7 @@ import SettingsPage from './pages/SettingsPage'; import LoginPage from './pages/LoginPage'; 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 { ProtectedRoute } from './auth/ProtectedRoute'; @@ -91,6 +92,14 @@ export function Routes() { } /> + + + + } + /> { + key: keyof T; + header: string; + render?: (value: any, 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); +} + +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 ( +
Loading...
+ ); + } + + 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]} +
+
+
+ ); +} diff --git a/frontend/src/generated/anthoLumeAPIV1.ts b/frontend/src/generated/anthoLumeAPIV1.ts index 72752e7..ed83d40 100644 --- a/frontend/src/generated/anthoLumeAPIV1.ts +++ b/frontend/src/generated/anthoLumeAPIV1.ts @@ -34,24 +34,34 @@ import type { import type { ActivityResponse, CreateDocumentBody, + DirectoryListResponse, DocumentResponse, DocumentsResponse, ErrorResponse, GetActivityParams, + GetAdmin200, GetDocumentsParams, + GetImportDirectoryParams, + GetLogsParams, GetProgressListParams, GetSearchParams, GraphDataResponse, HomeResponse, + ImportResultsResponse, LoginRequest, LoginResponse, + LogsResponse, + PostAdminActionBody, + PostImportBody, PostSearchBody, ProgressListResponse, ProgressResponse, SearchResponse, SettingsResponse, StreaksResponse, - UserStatisticsResponse + UpdateUserBody, + UserStatisticsResponse, + UsersResponse } from './model'; @@ -1412,3 +1422,670 @@ export const usePostSearch = , return useMutation(mutationOptions, queryClient); } +/** + * @summary Get admin page data + */ +export const getAdmin = ( + options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/admin`,options + ); + } + + + + +export const getGetAdminQueryKey = () => { + return [ + `/api/v1/admin` + ] as const; + } + + +export const getGetAdminQueryOptions = >, TError = AxiosError>( options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetAdminQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getAdmin({ signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetAdminQueryResult = NonNullable>> +export type GetAdminQueryError = AxiosError + + +export function useGetAdmin>, TError = AxiosError>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetAdmin>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetAdmin>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get admin page data + */ + +export function useGetAdmin>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetAdminQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Perform admin action (backup, restore, etc.) + */ +export const postAdminAction = ( + postAdminActionBody: PostAdminActionBody, options?: AxiosRequestConfig + ): Promise> => { + + const formUrlEncoded = new URLSearchParams(); +formUrlEncoded.append(`action`, postAdminActionBody.action) +if(postAdminActionBody.backup_types !== undefined) { + postAdminActionBody.backup_types.forEach(value => formUrlEncoded.append(`backup_types`, value)); + } +if(postAdminActionBody.restore_file !== undefined) { + formUrlEncoded.append(`restore_file`, postAdminActionBody.restore_file) + } + + return axios.default.post( + `/api/v1/admin`, + formUrlEncoded,{ + responseType: 'blob', + ...options,} + ); + } + + + +export const getPostAdminActionMutationOptions = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: PostAdminActionBody}, TContext>, axios?: AxiosRequestConfig} +): UseMutationOptions>, TError,{data: PostAdminActionBody}, TContext> => { + +const mutationKey = ['postAdminAction']; +const {mutation: mutationOptions, axios: axiosOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, axios: undefined}; + + + + + const mutationFn: MutationFunction>, {data: PostAdminActionBody}> = (props) => { + const {data} = props ?? {}; + + return postAdminAction(data,axiosOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type PostAdminActionMutationResult = NonNullable>> + export type PostAdminActionMutationBody = PostAdminActionBody + export type PostAdminActionMutationError = AxiosError + + /** + * @summary Perform admin action (backup, restore, etc.) + */ +export const usePostAdminAction = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: PostAdminActionBody}, TContext>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: PostAdminActionBody}, + TContext + > => { + + const mutationOptions = getPostAdminActionMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + +/** + * @summary Get all users + */ +export const getUsers = ( + options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/admin/users`,options + ); + } + + + + +export const getGetUsersQueryKey = () => { + return [ + `/api/v1/admin/users` + ] as const; + } + + +export const getGetUsersQueryOptions = >, TError = AxiosError>( options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetUsersQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getUsers({ signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetUsersQueryResult = NonNullable>> +export type GetUsersQueryError = AxiosError + + +export function useGetUsers>, TError = AxiosError>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetUsers>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetUsers>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get all users + */ + +export function useGetUsers>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetUsersQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Create, update, or delete user + */ +export const updateUser = ( + updateUserBody: UpdateUserBody, options?: AxiosRequestConfig + ): 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()) + } + + return axios.default.post( + `/api/v1/admin/users`, + formUrlEncoded,options + ); + } + + + +export const getUpdateUserMutationOptions = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: UpdateUserBody}, TContext>, axios?: AxiosRequestConfig} +): UseMutationOptions>, TError,{data: UpdateUserBody}, TContext> => { + +const mutationKey = ['updateUser']; +const {mutation: mutationOptions, axios: axiosOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, axios: undefined}; + + + + + const mutationFn: MutationFunction>, {data: UpdateUserBody}> = (props) => { + const {data} = props ?? {}; + + return updateUser(data,axiosOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type UpdateUserMutationResult = NonNullable>> + export type UpdateUserMutationBody = UpdateUserBody + export type UpdateUserMutationError = AxiosError + + /** + * @summary Create, update, or delete user + */ +export const useUpdateUser = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: UpdateUserBody}, TContext>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: UpdateUserBody}, + TContext + > => { + + const mutationOptions = getUpdateUserMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + +/** + * @summary Get import directory list + */ +export const getImportDirectory = ( + params?: GetImportDirectoryParams, options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/admin/import`,{ + ...options, + params: {...params, ...options?.params},} + ); + } + + + + +export const getGetImportDirectoryQueryKey = (params?: GetImportDirectoryParams,) => { + return [ + `/api/v1/admin/import`, ...(params ? [params]: []) + ] as const; + } + + +export const getGetImportDirectoryQueryOptions = >, TError = AxiosError>(params?: GetImportDirectoryParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetImportDirectoryQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getImportDirectory(params, { signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetImportDirectoryQueryResult = NonNullable>> +export type GetImportDirectoryQueryError = AxiosError + + +export function useGetImportDirectory>, TError = AxiosError>( + params: undefined | GetImportDirectoryParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetImportDirectory>, TError = AxiosError>( + params?: GetImportDirectoryParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetImportDirectory>, TError = AxiosError>( + params?: GetImportDirectoryParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get import directory list + */ + +export function useGetImportDirectory>, TError = AxiosError>( + params?: GetImportDirectoryParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetImportDirectoryQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Perform import + */ +export const postImport = ( + postImportBody: PostImportBody, options?: AxiosRequestConfig + ): Promise> => { + + const formUrlEncoded = new URLSearchParams(); +formUrlEncoded.append(`directory`, postImportBody.directory) +formUrlEncoded.append(`type`, postImportBody.type) + + return axios.default.post( + `/api/v1/admin/import`, + formUrlEncoded,options + ); + } + + + +export const getPostImportMutationOptions = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: PostImportBody}, TContext>, axios?: AxiosRequestConfig} +): UseMutationOptions>, TError,{data: PostImportBody}, TContext> => { + +const mutationKey = ['postImport']; +const {mutation: mutationOptions, axios: axiosOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, axios: undefined}; + + + + + const mutationFn: MutationFunction>, {data: PostImportBody}> = (props) => { + const {data} = props ?? {}; + + return postImport(data,axiosOptions) + } + + + + + return { mutationFn, ...mutationOptions }} + + export type PostImportMutationResult = NonNullable>> + export type PostImportMutationBody = PostImportBody + export type PostImportMutationError = AxiosError + + /** + * @summary Perform import + */ +export const usePostImport = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: PostImportBody}, TContext>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: PostImportBody}, + TContext + > => { + + const mutationOptions = getPostImportMutationOptions(options); + + return useMutation(mutationOptions, queryClient); + } + +/** + * @summary Get import results + */ +export const getImportResults = ( + options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/admin/import-results`,options + ); + } + + + + +export const getGetImportResultsQueryKey = () => { + return [ + `/api/v1/admin/import-results` + ] as const; + } + + +export const getGetImportResultsQueryOptions = >, TError = AxiosError>( options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetImportResultsQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getImportResults({ signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetImportResultsQueryResult = NonNullable>> +export type GetImportResultsQueryError = AxiosError + + +export function useGetImportResults>, TError = AxiosError>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetImportResults>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetImportResults>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get import results + */ + +export function useGetImportResults>, TError = AxiosError>( + options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetImportResultsQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + +/** + * @summary Get logs with optional filter + */ +export const getLogs = ( + params?: GetLogsParams, options?: AxiosRequestConfig + ): Promise> => { + + + return axios.default.get( + `/api/v1/admin/logs`,{ + ...options, + params: {...params, ...options?.params},} + ); + } + + + + +export const getGetLogsQueryKey = (params?: GetLogsParams,) => { + return [ + `/api/v1/admin/logs`, ...(params ? [params]: []) + ] as const; + } + + +export const getGetLogsQueryOptions = >, TError = AxiosError>(params?: GetLogsParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} +) => { + +const {query: queryOptions, axios: axiosOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetLogsQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getLogs(params, { signal, ...axiosOptions }); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetLogsQueryResult = NonNullable>> +export type GetLogsQueryError = AxiosError + + +export function useGetLogs>, TError = AxiosError>( + params: undefined | GetLogsParams, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetLogs>, TError = AxiosError>( + params?: GetLogsParams, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetLogs>, TError = AxiosError>( + params?: GetLogsParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get logs with optional filter + */ + +export function useGetLogs>, TError = AxiosError>( + params?: GetLogsParams, options?: { query?:Partial>, TError, TData>>, axios?: AxiosRequestConfig} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetLogsQueryOptions(params,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + + diff --git a/frontend/src/generated/model/activity.ts b/frontend/src/generated/model/activity.ts index bc38a8b..772a1fb 100644 --- a/frontend/src/generated/model/activity.ts +++ b/frontend/src/generated/model/activity.ts @@ -7,9 +7,13 @@ */ export interface Activity { - id: string; - user_id: string; document_id: string; - activity_type: string; - timestamp: 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/backupType.ts b/frontend/src/generated/model/backupType.ts new file mode 100644 index 0000000..09bf2e1 --- /dev/null +++ b/frontend/src/generated/model/backupType.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const BackupType = { + COVERS: 'COVERS', + DOCUMENTS: 'DOCUMENTS', +} as const; diff --git a/frontend/src/generated/model/directoryItem.ts b/frontend/src/generated/model/directoryItem.ts new file mode 100644 index 0000000..70b4d02 --- /dev/null +++ b/frontend/src/generated/model/directoryItem.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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..0f10ccc --- /dev/null +++ b/frontend/src/generated/model/directoryListResponse.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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/getAdmin200.ts b/frontend/src/generated/model/getAdmin200.ts new file mode 100644 index 0000000..0a6f7c7 --- /dev/null +++ b/frontend/src/generated/model/getAdmin200.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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/getImportDirectoryParams.ts b/frontend/src/generated/model/getImportDirectoryParams.ts new file mode 100644 index 0000000..eb31466 --- /dev/null +++ b/frontend/src/generated/model/getImportDirectoryParams.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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..61ea16e --- /dev/null +++ b/frontend/src/generated/model/getLogsParams.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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; +}; diff --git a/frontend/src/generated/model/importResult.ts b/frontend/src/generated/model/importResult.ts new file mode 100644 index 0000000..ef92996 --- /dev/null +++ b/frontend/src/generated/model/importResult.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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..c7f6d7c --- /dev/null +++ b/frontend/src/generated/model/importResultStatus.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +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..833e1e2 --- /dev/null +++ b/frontend/src/generated/model/importResultsResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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..ee7b0d8 --- /dev/null +++ b/frontend/src/generated/model/importType.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +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 index 4bfa505..f4cffdf 100644 --- a/frontend/src/generated/model/index.ts +++ b/frontend/src/generated/model/index.ts @@ -8,25 +8,41 @@ export * from './activity'; export * from './activityResponse'; +export * from './backupType'; 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 './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 './importResultStatus'; +export * from './importResultsResponse'; +export * from './importType'; export * from './leaderboardData'; export * from './leaderboardEntry'; +export * from './logEntry'; export * from './loginRequest'; export * from './loginResponse'; +export * from './logsResponse'; +export * from './operationType'; +export * from './postAdminActionBody'; +export * from './postAdminActionBodyAction'; +export * from './postImportBody'; export * from './postSearchBody'; export * from './progress'; export * from './progressListResponse'; @@ -36,7 +52,10 @@ export * from './searchResponse'; export * from './setting'; export * from './settingsResponse'; export * from './streaksResponse'; +export * from './updateUserBody'; +export * from './user'; export * from './userData'; export * from './userStatisticsResponse'; export * from './userStreak'; +export * from './usersResponse'; export * from './wordCount'; \ No newline at end of file diff --git a/frontend/src/generated/model/logEntry.ts b/frontend/src/generated/model/logEntry.ts new file mode 100644 index 0000000..e1e86a8 --- /dev/null +++ b/frontend/src/generated/model/logEntry.ts @@ -0,0 +1,9 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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/logsResponse.ts b/frontend/src/generated/model/logsResponse.ts new file mode 100644 index 0000000..38085f6 --- /dev/null +++ b/frontend/src/generated/model/logsResponse.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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; +} diff --git a/frontend/src/generated/model/operationType.ts b/frontend/src/generated/model/operationType.ts new file mode 100644 index 0000000..64f5e59 --- /dev/null +++ b/frontend/src/generated/model/operationType.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +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..e669e6c --- /dev/null +++ b/frontend/src/generated/model/postAdminActionBody.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { PostAdminActionBodyAction } from './postAdminActionBodyAction'; +import type { BackupType } from './backupType'; + +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..eef1fab --- /dev/null +++ b/frontend/src/generated/model/postAdminActionBodyAction.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +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..086b12f --- /dev/null +++ b/frontend/src/generated/model/postImportBody.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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/updateUserBody.ts b/frontend/src/generated/model/updateUserBody.ts new file mode 100644 index 0000000..3cf5907 --- /dev/null +++ b/frontend/src/generated/model/updateUserBody.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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/user.ts b/frontend/src/generated/model/user.ts new file mode 100644 index 0000000..348bbd2 --- /dev/null +++ b/frontend/src/generated/model/user.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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/usersResponse.ts b/frontend/src/generated/model/usersResponse.ts new file mode 100644 index 0000000..0bbf252 --- /dev/null +++ b/frontend/src/generated/model/usersResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.21.0 🍺 + * 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/pages/ActivityPage.tsx b/frontend/src/pages/ActivityPage.tsx index 0021ab8..ea79cff 100644 --- a/frontend/src/pages/ActivityPage.tsx +++ b/frontend/src/pages/ActivityPage.tsx @@ -1,43 +1,53 @@ +import { Link } from 'react-router-dom'; import { useGetActivity } from '../generated/anthoLumeAPIV1'; +import { Table } from '../components/Table'; export default function ActivityPage() { const { data, isLoading } = useGetActivity({ offset: 0, limit: 100 }); const activities = data?.data?.activities; - if (isLoading) { - return
Loading...
; - } + const columns = [ + { + key: 'document_id' as const, + header: 'Document', + render: (_: any, row: any) => ( + + {row.author || 'Unknown'} - {row.title || 'Unknown'} + + ), + }, + { + key: 'start_time' as const, + header: 'Time', + render: (value: any) => value || 'N/A', + }, + { + key: 'duration' as const, + header: 'Duration', + render: (value: any) => { + if (!value) return 'N/A'; + // Format duration (in seconds) to readable format + const hours = Math.floor(value / 3600); + const minutes = Math.floor((value % 3600) / 60); + const seconds = value % 60; + if (hours > 0) { + return `${hours}h ${minutes}m ${seconds}s`; + } else if (minutes > 0) { + return `${minutes}m ${seconds}s`; + } else { + return `${seconds}s`; + } + }, + }, + { + key: 'end_percentage' as const, + header: 'Percent', + render: (value: any) => (value != null ? `${value}%` : '0%'), + }, + ]; - return ( -
-
- - - - - - - - - - {activities?.map((activity: any) => ( - - - - - - ))} - -
Activity TypeDocumentTimestamp
- {activity.activity_type} - - - {activity.document_id} - - - {new Date(activity.timestamp).toLocaleString()} -
-
-
- ); -} \ No newline at end of file + return ; +} diff --git a/frontend/src/pages/AdminImportPage.tsx b/frontend/src/pages/AdminImportPage.tsx index 1affc4d..bef7390 100644 --- a/frontend/src/pages/AdminImportPage.tsx +++ b/frontend/src/pages/AdminImportPage.tsx @@ -1,8 +1,181 @@ +import { useState } from 'react'; +import { useGetImportDirectory, usePostImport } from '../generated/anthoLumeAPIV1'; +import { Button } from '../components/Button'; +import { FolderOpen } from 'lucide-react'; + export default function AdminImportPage() { + const [currentPath, setCurrentPath] = useState(''); + const [selectedDirectory, setSelectedDirectory] = useState(''); + const [importType, setImportType] = useState<'DIRECT' | 'COPY'>('DIRECT'); + + const { data: directoryData, isLoading } = useGetImportDirectory( + currentPath ? { directory: currentPath } : {} + ); + + const postImport = usePostImport(); + + const directories = directoryData?.data?.items || []; + const currentPathDisplay = directoryData?.data?.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) => { + console.log('Import completed:', response.data); + // Redirect to import results page + window.location.href = '/admin/import-results'; + }, + onError: (error) => { + console.error('Import failed:', error); + alert('Import failed: ' + (error as any).message); + }, + } + ); + }; + + const handleCancel = () => { + setSelectedDirectory(''); + }; + + if (isLoading && !currentPath) { + return
Loading...
; + } + + if (selectedDirectory) { + return ( +
+
+
+

+ Selected Import Directory +

+
+
+
+ +

+ {selectedDirectory} +

+
+
+
+ setImportType('DIRECT')} + /> + +
+
+ setImportType('COPY')} + /> + +
+
+
+
+ + +
+ +
+
+
+ ); + } + return ( -
-

Admin - Import

-

Document import page

+
+
+
+ + + + + + + + {currentPath !== '/' && ( + + + + + )} + {directories.length === 0 ? ( + + + + ) : ( + directories.map((item) => ( + + + + + )) + )} + +
+ {currentPath} +
+ +
No Folders
+ + + +
+ ); } \ No newline at end of file diff --git a/frontend/src/pages/AdminImportResultsPage.tsx b/frontend/src/pages/AdminImportResultsPage.tsx new file mode 100644 index 0000000..e8fdc3f --- /dev/null +++ b/frontend/src/pages/AdminImportResultsPage.tsx @@ -0,0 +1,73 @@ +import { useGetImportResults } from '../generated/anthoLumeAPIV1'; +import type { ImportResult } from '../generated/model/importResult'; +import { Link } from 'react-router-dom'; + +export default function AdminImportResultsPage() { + const { data: resultsData, isLoading } = useGetImportResults(); + const results = resultsData?.data?.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 || ''}

+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/AdminLogsPage.tsx b/frontend/src/pages/AdminLogsPage.tsx index d8565db..6c34434 100644 --- a/frontend/src/pages/AdminLogsPage.tsx +++ b/frontend/src/pages/AdminLogsPage.tsx @@ -1,8 +1,66 @@ +import { useState, FormEvent } from 'react'; +import { useGetLogs } from '../generated/anthoLumeAPIV1'; +import { Button } from '../components/Button'; +import { Search } from 'lucide-react'; + export default function AdminLogsPage() { + const [filter, setFilter] = useState(''); + + const { data: logsData, isLoading, refetch } = useGetLogs( + filter ? { filter } : {} + ); + + const logs = logsData?.data?.logs || []; + + const handleFilterSubmit = (e: FormEvent) => { + e.preventDefault(); + refetch(); + }; + + if (isLoading) { + return
Loading...
; + } + return (
-

Admin - Logs

-

System logs page

+ {/* Filter Form */} +
+
+
+
+ + + + setFilter(e.target.value)} + className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" + placeholder="JQ Filter" + /> +
+
+
+ +
+
+
+ + {/* Log Display */} +
+ {logs.map((log: string, index: number) => ( + + {log} + + ))} +
); } \ No newline at end of file diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index 7a53015..6783d11 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -1,8 +1,218 @@ +import { useState, FormEvent } from 'react'; +import { useGetAdmin, usePostAdminAction } from '../generated/anthoLumeAPIV1'; +import { Button } from '../components/Button'; + +interface BackupTypes { + covers: boolean; + documents: boolean; +} + export default function AdminPage() { + const { isLoading } = useGetAdmin(); + const postAdminAction = usePostAdminAction(); + + const [backupTypes, setBackupTypes] = useState({ + covers: false, + documents: false, + }); + const [restoreFile, setRestoreFile] = useState(null); + const [message, setMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + + const handleBackupSubmit = (e: FormEvent) => { + e.preventDefault(); + const backupTypesList: string[] = []; + if (backupTypes.covers) backupTypesList.push('COVERS'); + if (backupTypes.documents) backupTypesList.push('DOCUMENTS'); + + postAdminAction.mutate( + { + data: { + action: 'BACKUP', + backup_types: backupTypesList as any, + }, + }, + { + onSuccess: (response) => { + // Handle file download + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `AnthoLumeBackup_${new Date().toISOString().replace(/[:.]/g, '')}.zip`); + document.body.appendChild(link); + link.click(); + link.remove(); + setMessage('Backup completed successfully'); + setErrorMessage(null); + }, + onError: (error) => { + setErrorMessage('Backup failed: ' + (error as any).message); + setMessage(null); + }, + } + ); + }; + + const handleRestoreSubmit = (e: FormEvent) => { + e.preventDefault(); + if (!restoreFile) return; + + const formData = new FormData(); + formData.append('restore_file', restoreFile); + formData.append('action', 'RESTORE'); + + postAdminAction.mutate( + { + data: formData as any, + }, + { + onSuccess: () => { + setMessage('Restore completed successfully'); + setErrorMessage(null); + }, + onError: (error) => { + setErrorMessage('Restore failed: ' + (error as any).message); + setMessage(null); + }, + } + ); + }; + + const handleMetadataMatch = () => { + postAdminAction.mutate( + { + data: { + action: 'METADATA_MATCH', + }, + }, + { + onSuccess: () => { + setMessage('Metadata matching started'); + setErrorMessage(null); + }, + onError: (error) => { + setErrorMessage('Metadata matching failed: ' + (error as any).message); + setMessage(null); + }, + } + ); + }; + + const handleCacheTables = () => { + postAdminAction.mutate( + { + data: { + action: 'CACHE_TABLES', + }, + }, + { + onSuccess: () => { + setMessage('Cache tables started'); + setErrorMessage(null); + }, + onError: (error) => { + setErrorMessage('Cache tables failed: ' + (error as any).message); + setMessage(null); + }, + } + ); + }; + + if (isLoading) { + return
Loading...
; + } + return ( -
-

Admin - General

-

Admin general settings page

+
+ {/* Backup & Restore Card */} +
+

Backup & Restore

+
+ {/* Backup Form */} +
+
+
+ setBackupTypes({ ...backupTypes, covers: e.target.checked })} + /> + +
+
+ setBackupTypes({ ...backupTypes, documents: e.target.checked })} + /> + +
+
+
+ +
+
+ + {/* Restore Form */} +
+
+ setRestoreFile(e.target.files?.[0] || null)} + className="w-full" + /> +
+
+ +
+
+
+ {errorMessage && ( + {errorMessage} + )} + {message && ( + {message} + )} +
+ + {/* Tasks Card */} +
+

Tasks

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

Metadata Matching

+
+
+ +
+
+

Cache Tables

+
+
+ +
+
+
); } \ No newline at end of file diff --git a/frontend/src/pages/AdminUsersPage.tsx b/frontend/src/pages/AdminUsersPage.tsx index e420fda..9e9a352 100644 --- a/frontend/src/pages/AdminUsersPage.tsx +++ b/frontend/src/pages/AdminUsersPage.tsx @@ -1,8 +1,234 @@ +import { useState, FormEvent } from 'react'; +import { useGetUsers, useUpdateUser } from '../generated/anthoLumeAPIV1'; +import { Plus, Trash2 } from 'lucide-react'; + export default function AdminUsersPage() { + const { data: usersData, isLoading, refetch } = useGetUsers({}); + const updateUser = useUpdateUser(); + + const [showAddForm, setShowAddForm] = useState(false); + const [newUsername, setNewUsername] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [newIsAdmin, setNewIsAdmin] = useState(false); + + const users = usersData?.data?.users || []; + + const handleCreateUser = (e: FormEvent) => { + e.preventDefault(); + if (!newUsername || !newPassword) return; + + updateUser.mutate( + { + data: { + operation: 'CREATE', + user: newUsername, + password: newPassword, + is_admin: newIsAdmin, + }, + }, + { + onSuccess: () => { + setShowAddForm(false); + setNewUsername(''); + setNewPassword(''); + setNewIsAdmin(false); + refetch(); + }, + onError: (error: any) => { + alert('Failed to create user: ' + error.message); + }, + } + ); + }; + + const handleDeleteUser = (userId: string) => { + updateUser.mutate( + { + data: { + operation: 'DELETE', + user: userId, + }, + }, + { + onSuccess: () => { + refetch(); + }, + onError: (error: any) => { + alert('Failed to delete user: ' + error.message); + }, + } + ); + }; + + const handleUpdatePassword = (userId: string, password: string) => { + if (!password) return; + + updateUser.mutate( + { + data: { + operation: 'UPDATE', + user: userId, + password: password, + }, + }, + { + onSuccess: () => { + refetch(); + }, + onError: (error: any) => { + alert('Failed to update password: ' + error.message); + }, + } + ); + }; + + const handleToggleAdmin = (userId: string, isAdmin: boolean) => { + updateUser.mutate( + { + data: { + operation: 'UPDATE', + user: userId, + is_admin: isAdmin, + }, + }, + { + onSuccess: () => { + refetch(); + }, + onError: (error: any) => { + alert('Failed to update admin status: ' + error.message); + }, + } + ); + }; + + if (isLoading) { + return
Loading...
; + } + return ( -
-

Admin - Users

-

User management page

+
+ {/* Add User Form */} + {showAddForm && ( +
+
+ setNewUsername(e.target.value)} + placeholder="Username" + className="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white" + /> + setNewPassword(e.target.value)} + placeholder="Password" + className="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white" + /> +
+ setNewIsAdmin(e.target.checked)} + /> + +
+ +
+
+ )} + + {/* Users Table */} +
+ + + + + + + + + + + + {users.length === 0 ? ( + + + + ) : ( + users.map((user) => ( + + {/* Delete Button */} + + {/* User ID */} + + {/* Password Reset */} + + {/* Admin Toggle */} + + {/* Created Date */} + + + )) + )} + +
+ + UserPassword + Permissions + Created
No Results
+ + +

{user.id}

+
+ + + + + +

{user.created_at}

+
+
); } \ No newline at end of file diff --git a/frontend/src/pages/ProgressPage.tsx b/frontend/src/pages/ProgressPage.tsx index b227e85..cf3dbe1 100644 --- a/frontend/src/pages/ProgressPage.tsx +++ b/frontend/src/pages/ProgressPage.tsx @@ -1,51 +1,40 @@ import { Link } from 'react-router-dom'; import { useGetProgressList } from '../generated/anthoLumeAPIV1'; +import { Table } from '../components/Table'; export default function ProgressPage() { const { data, isLoading } = useGetProgressList({ page: 1, limit: 15 }); const progress = data?.data?.progress; - if (isLoading) { - return
Loading...
; - } + const columns = [ + { + key: 'document_id' as const, + header: 'Document', + render: (_: any, row: any) => ( + + {row.author || 'Unknown'} - {row.title || 'Unknown'} + + ), + }, + { + key: 'device_name' as const, + header: 'Device Name', + render: (value: any) => value || 'Unknown', + }, + { + key: 'percentage' as const, + header: 'Percentage', + render: (value: any) => (value ? `${Math.round(value)}%` : '0%'), + }, + { + key: 'created_at' as const, + header: 'Created At', + render: (value: any) => (value ? new Date(value).toLocaleDateString() : 'N/A'), + }, + ]; - return ( -
-
- - - - - - - - - - - {progress?.map((row: any) => ( - - - - - - - ))} - -
DocumentDevice NamePercentageCreated At
- - {row.author || 'Unknown'} - {row.title || 'Unknown'} - - - {row.device_name || 'Unknown'} - - {row.percentage ? Math.round(row.percentage) : 0}% - - {row.created_at ? new Date(row.created_at).toLocaleDateString() : 'N/A'} -
-
-
- ); -} \ No newline at end of file + return ; +}