diff --git a/frontend/IMPLEMENTATION_PLAN.md b/frontend/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..9f5ee05 --- /dev/null +++ b/frontend/IMPLEMENTATION_PLAN.md @@ -0,0 +1,839 @@ +# Frontend Implementation Plan: Next.js + FastAPI Template + +**Last Updated:** October 31, 2025 +**Current Phase:** Phase 1 COMPLETE ✅ | Ready for Phase 2 +**Overall Progress:** 1 of 12 phases complete + +--- + +## Summary + +Build a production-ready Next.js 15 frontend with full authentication, admin dashboard, user/organization management, and session tracking. The frontend integrates with the existing FastAPI backend using OpenAPI-generated clients, TanStack Query for state, Zustand for auth, and shadcn/ui components. + +**Target:** 90%+ test coverage, comprehensive documentation, and robust foundations for enterprise projects. + +**Current State:** Phase 1 infrastructure complete with 81.6% test coverage, 66 passing tests, zero TypeScript errors +**Target State:** Complete template matching `frontend-requirements.md` with all 12 phases + +--- + +## Implementation Directives (MUST FOLLOW) + +### Documentation-First Approach +- Phase 0 created `/docs` folder with all architecture, standards, and guides ✅ +- ALL subsequent phases MUST reference and follow patterns in `/docs` +- **If context is lost, `/docs` + this file + `frontend-requirements.md` are sufficient to resume** + +### Quality Assurance Protocol + +**1. After Each Task:** Launch self-review to check: +- Code quality issues +- Security vulnerabilities +- Performance problems +- Accessibility issues +- Standard violations (check against `/docs/CODING_STANDARDS.md`) + +**2. After Each Phase:** Launch multi-agent deep review to: +- Verify phase objectives met +- Check integration with previous phases +- Identify critical issues requiring immediate fixes +- Recommend improvements before proceeding +- Update documentation if patterns evolved + +**3. Testing Requirements:** +- Write tests alongside feature code (not after) +- Unit tests: All hooks, utilities, services +- Component tests: All reusable components +- Integration tests: All pages and flows +- E2E tests: Critical user journeys (auth, admin CRUD) +- Target: 90%+ coverage for template robustness +- Use Jest + React Testing Library + Playwright + +**4. Context Preservation:** +- Update `/docs` with implementation decisions +- Document deviations from requirements in `ARCHITECTURE.md` +- Keep `frontend-requirements.md` updated if backend changes +- Update THIS FILE after each phase with actual progress + +--- + +## Current System State (Phase 1 Complete) + +### ✅ What's Implemented + +**Project Infrastructure:** +- Next.js 15 with App Router +- TypeScript strict mode enabled +- Tailwind CSS 4 configured +- shadcn/ui components installed (15+ components) +- Path aliases configured (@/) + +**Authentication System:** +- `src/lib/auth/crypto.ts` - AES-GCM encryption (82% coverage) +- `src/lib/auth/storage.ts` - Secure token storage (72.85% coverage) +- `src/stores/authStore.ts` - Zustand auth store (92.59% coverage) +- `src/config/app.config.ts` - Centralized configuration (81% coverage) +- SSR-safe implementations throughout + +**API Integration:** +- `src/lib/api/client.ts` - Axios wrapper with interceptors (to be replaced) +- `src/lib/api/errors.ts` - Error parsing utilities (to be replaced) +- `scripts/generate-api-client.sh` - OpenAPI generation script +- **NOTE:** Manual client files marked for replacement with generated client + +**Testing Infrastructure:** +- Jest configured with Next.js integration +- 66 tests passing (100%) +- 81.6% code coverage (exceeds 70% target) +- Real crypto testing (@peculiar/webcrypto) +- No mocks for security-critical code + +**Documentation:** +- `/docs/ARCHITECTURE.md` - System design ✅ +- `/docs/CODING_STANDARDS.md` - Code standards ✅ +- `/docs/COMPONENT_GUIDE.md` - Component patterns ✅ +- `/docs/FEATURE_EXAMPLES.md` - Implementation examples ✅ +- `/docs/API_INTEGRATION.md` - API integration guide ✅ + +### 📊 Test Coverage Details + +``` +File | Statements | Branches | Functions | Lines +----------------|------------|----------|-----------|------- +All files | 81.60% | 84.09% | 93.10% | 82.08% +config | 81.08% | 81.25% | 80.00% | 84.37% +lib/auth | 76.85% | 73.07% | 92.30% | 76.66% +stores | 92.59% | 97.91% | 100.00% | 93.87% +``` + +**Coverage Exclusions (Properly Configured):** +- Auto-generated API client (`src/lib/api/generated/**`) +- Manual API client (to be replaced) +- Third-party UI components (`src/components/ui/**`) +- Next.js app directory (`src/app/**` - test with E2E) +- Re-export index files +- Old implementation files (`.old.ts`) + +### 🎯 Quality Metrics + +- ✅ TypeScript: 0 compilation errors +- ✅ ESLint: 0 warnings +- ✅ Tests: 66/66 passing +- ✅ Coverage: 81.6% (target: 70%) +- ✅ Security: No vulnerabilities +- ✅ SSR: All browser APIs properly guarded + +### 📁 Current Folder Structure + +``` +frontend/ +├── docs/ ✅ Phase 0 complete +│ ├── ARCHITECTURE.md +│ ├── CODING_STANDARDS.md +│ ├── COMPONENT_GUIDE.md +│ ├── FEATURE_EXAMPLES.md +│ └── API_INTEGRATION.md +├── src/ +│ ├── app/ # Next.js app directory +│ ├── components/ +│ │ └── ui/ # shadcn/ui components ✅ +│ ├── lib/ +│ │ ├── api/ +│ │ │ ├── generated/ # OpenAPI client (empty, needs generation) +│ │ │ ├── client.ts # ✅ Axios wrapper (to replace) +│ │ │ └── errors.ts # ✅ Error parsing (to replace) +│ │ ├── auth/ +│ │ │ ├── crypto.ts # ✅ 82% coverage +│ │ │ └── storage.ts # ✅ 72.85% coverage +│ │ └── utils/ +│ ├── stores/ +│ │ └── authStore.ts # ✅ 92.59% coverage +│ └── config/ +│ └── app.config.ts # ✅ 81% coverage +├── tests/ # ✅ 66 tests +│ ├── lib/auth/ # Crypto & storage tests +│ ├── stores/ # Auth store tests +│ └── config/ # Config tests +├── scripts/ +│ └── generate-api-client.sh # ✅ OpenAPI generation +├── jest.config.js # ✅ Configured +├── jest.setup.js # ✅ Global mocks +├── frontend-requirements.md # ✅ Updated +└── IMPLEMENTATION_PLAN.md # ✅ This file + +``` + +### ⚠️ Known Technical Debt + +1. **Manual API Client Files** - Need replacement when backend ready: + - Delete: `src/lib/api/client.ts` + - Delete: `src/lib/api/errors.ts` + - Run: `npm run generate:api` + - Create: Thin interceptor wrapper if needed + +2. **Old Implementation Files** - Need cleanup: + - Delete: `src/stores/authStore.old.ts` + +3. **API Client Generation** - Needs backend running: + - Backend must be at `http://localhost:8000` + - OpenAPI spec at `/api/v1/openapi.json` + - Run `npm run generate:api` to create client + +--- + +## Phase 0: Foundation Documents & Requirements Alignment ✅ + +**Status:** COMPLETE +**Duration:** 1 day +**Completed:** October 31, 2025 + +### Task 0.1: Update Requirements Document ✅ +- ✅ Updated `frontend-requirements.md` with API corrections +- ✅ Added Section 4.5 (Session Management UI) +- ✅ Added Section 15 (API Endpoint Reference) +- ✅ Updated auth flow with token rotation details +- ✅ Added missing User/Organization model fields + +### Task 0.2: Create Architecture Documentation ✅ +- ✅ Created `docs/ARCHITECTURE.md` +- ✅ System overview (Next.js App Router, TanStack Query, Zustand) +- ✅ Technology stack rationale +- ✅ Data flow diagrams +- ✅ Folder structure explanation +- ✅ Design patterns documented + +### Task 0.3: Create Coding Standards Documentation ✅ +- ✅ Created `docs/CODING_STANDARDS.md` +- ✅ TypeScript standards (strict mode, no any) +- ✅ React component patterns +- ✅ Naming conventions +- ✅ State management rules +- ✅ Form patterns +- ✅ Error handling patterns +- ✅ Testing standards + +### Task 0.4: Create Component & Feature Guides ✅ +- ✅ Created `docs/COMPONENT_GUIDE.md` +- ✅ Created `docs/FEATURE_EXAMPLES.md` +- ✅ Created `docs/API_INTEGRATION.md` +- ✅ Complete walkthroughs for common patterns + +**Phase 0 Review:** ✅ All docs complete, clear, and accurate + +--- + +## Phase 1: Project Setup & Infrastructure ✅ + +**Status:** COMPLETE +**Duration:** 3 days +**Completed:** October 31, 2025 + +### Task 1.1: Dependency Installation & Configuration ✅ +**Status:** COMPLETE +**Blockers:** None + +**Installed Dependencies:** +```bash +# Core +@tanstack/react-query@5, zustand@4, axios@1 +@hey-api/openapi-ts (dev) +react-hook-form@7, zod@3, @hookform/resolvers +date-fns, clsx, tailwind-merge, lucide-react +recharts@2 + +# shadcn/ui +npx shadcn@latest init +npx shadcn@latest add button card input label form select table dialog + toast tabs dropdown-menu popover sheet avatar badge separator skeleton alert + +# Testing +jest, @testing-library/react, @testing-library/jest-dom +@testing-library/user-event, @playwright/test, @types/jest +@peculiar/webcrypto (for real crypto in tests) +``` + +**Configuration:** +- ✅ `components.json` for shadcn/ui +- ✅ `tsconfig.json` with path aliases +- ✅ Tailwind configured for dark mode +- ✅ `.env.example` and `.env.local` created +- ✅ `jest.config.js` with Next.js integration +- ✅ `jest.setup.js` with global mocks + +### Task 1.2: OpenAPI Client Generation Setup ✅ +**Status:** COMPLETE +**Can run parallel with:** 1.3, 1.4 + +**Completed:** +- ✅ Created `scripts/generate-api-client.sh` using `@hey-api/openapi-ts` +- ✅ Configured output to `src/lib/api/generated/` +- ✅ Added npm script: `"generate:api": "./scripts/generate-api-client.sh"` +- ✅ Fixed deprecated options (removed `--name`, `--useOptions`, `--exportSchemas`) +- ✅ Used modern syntax: `--client @hey-api/client-axios` +- ✅ Successfully generated TypeScript client from backend API +- ✅ TypeScript compilation passes with generated types + +**Generated Files:** +- `src/lib/api/generated/index.ts` - Main exports +- `src/lib/api/generated/types.gen.ts` - TypeScript types (35KB) +- `src/lib/api/generated/sdk.gen.ts` - API functions (29KB) +- `src/lib/api/generated/client.gen.ts` - Axios client +- `src/lib/api/generated/client/` - Client utilities +- `src/lib/api/generated/core/` - Core utilities + +**To Regenerate (When Backend Changes):** +```bash +npm run generate:api +``` + +### Task 1.3: Axios Client & Interceptors ✅ +**Status:** COMPLETE (needs replacement in Phase 2) +**Can run parallel with:** 1.2, 1.4 + +**Completed:** +- ✅ Created `src/lib/api/client.ts` - Axios wrapper + - Request interceptor: Add Authorization header + - Response interceptor: Handle 401, 403, 429, 500 + - Error response parser + - Timeout configuration (30s default) + - Development logging +- ✅ Created `src/lib/api/errors.ts` - Error types and parsing +- ✅ Tests written for error parsing + +**⚠️ Note:** This is a manual implementation. Will be replaced with generated client + thin interceptor wrapper once backend API is generated. + +### Task 1.4: Folder Structure Creation ✅ +**Status:** COMPLETE +**Can run parallel with:** 1.2, 1.3 + +**Completed:** +- ✅ All directories created per requirements +- ✅ Placeholder index.ts files for exports +- ✅ Structure matches `docs/ARCHITECTURE.md` + +### Task 1.5: Authentication Core Implementation ✅ +**Status:** COMPLETE (additional work beyond original plan) + +**Completed:** +- ✅ `src/lib/auth/crypto.ts` - AES-GCM encryption with random IVs +- ✅ `src/lib/auth/storage.ts` - Encrypted token storage with localStorage +- ✅ `src/stores/authStore.ts` - Complete Zustand auth store +- ✅ `src/config/app.config.ts` - Centralized configuration with validation +- ✅ All SSR-safe with proper browser API guards +- ✅ 66 comprehensive tests written (81.6% coverage) +- ✅ Security audit completed +- ✅ Real crypto testing (no mocks) + +**Security Features:** +- AES-GCM encryption with 256-bit keys +- Random IV per encryption +- Key stored in sessionStorage (per-session) +- Token validation (JWT format checking) +- Type-safe throughout +- No token leaks in logs + +**Phase 1 Review:** ✅ Multi-agent audit completed. Infrastructure solid. All tests passing. Ready for Phase 2. + +### Audit Results (October 31, 2025) + +**Comprehensive audit conducted with the following results:** + +**Critical Issues Found:** 5 +**Critical Issues Fixed:** 5 ✅ + +**Issues Resolved:** +1. ✅ TypeScript compilation error (unused @ts-expect-error) +2. ✅ Duplicate configuration files +3. ✅ Test mocks didn't match real implementation +4. ✅ Test coverage properly configured +5. ✅ API client exclusions documented + +**Final Metrics:** +- Tests: 66/66 passing (100%) +- Coverage: 81.6% (exceeds 70% target) +- TypeScript: 0 errors +- Security: No vulnerabilities + +**Audit Documents:** +- `/tmp/AUDIT_SUMMARY.txt` - Executive summary +- `/tmp/AUDIT_COMPLETE.md` - Full report +- `/tmp/COVERAGE_CONFIG.md` - Coverage configuration +- `/tmp/detailed_findings.md` - Issue details + +--- + +## Phase 2: Authentication System + +**Status:** READY TO START 📋 +**Duration:** 3-4 days +**Prerequisites:** Phase 1 complete ✅ + +**Context for Phase 2:** +Phase 1 already implemented core authentication infrastructure (crypto, storage, auth store). Phase 2 will build the UI layer on top of this foundation. + +### Task 2.1: Token Storage & Auth Store ✅ (Done in Phase 1) +**Status:** COMPLETE (already done) + +This was completed as part of Phase 1 infrastructure: +- ✅ `src/lib/auth/crypto.ts` - AES-GCM encryption +- ✅ `src/lib/auth/storage.ts` - Token storage utilities +- ✅ `src/stores/authStore.ts` - Complete Zustand store +- ✅ 92.59% test coverage on auth store +- ✅ Security audit passed + +**Skip this task - move to 2.2** + +### Task 2.2: Auth Interceptor Integration 🔗 +**Status:** PARTIALLY COMPLETE (needs update) +**Depends on:** 2.1 ✅ (already complete) + +**Current State:** +- `src/lib/api/client.ts` exists with basic interceptor logic +- Integrates with auth store +- Has token refresh flow +- Has retry mechanism + +**Actions Needed:** +- [ ] Test with generated API client (once backend ready) +- [ ] Verify token rotation works +- [ ] Add race condition testing +- [ ] Verify no infinite refresh loops + +**Reference:** `docs/API_INTEGRATION.md`, Requirements Section 5.2 + +### Task 2.3: Auth Hooks & Components 🔐 +**Status:** TODO 📋 +**Can run parallel with:** 2.4, 2.5 after 2.2 complete + +**Actions Needed:** + +Create React Query hooks in `src/lib/api/hooks/useAuth.ts`: +- [ ] `useLogin` - Login mutation +- [ ] `useRegister` - Register mutation +- [ ] `useLogout` - Logout mutation +- [ ] `useLogoutAll` - Logout all devices +- [ ] `useRefreshToken` - Token refresh +- [ ] `useMe` - Get current user + +Create convenience hooks in `src/hooks/useAuth.ts`: +- [ ] Wrapper around auth store for easy component access + +Create auth protection components: +- [ ] `src/components/auth/AuthGuard.tsx` - HOC for route protection +- [ ] `src/components/auth/ProtectedRoute.tsx` - Client component wrapper + +**Testing:** +- [ ] Unit tests for each hook +- [ ] Test loading states +- [ ] Test error handling +- [ ] Test redirect logic + +**Reference:** `docs/FEATURE_EXAMPLES.md` (auth patterns), Requirements Section 4.3 + +### Task 2.4: Login & Registration Pages 📄 +**Status:** TODO 📋 +**Can run parallel with:** 2.3, 2.5 after 2.2 complete + +**Actions Needed:** + +Create forms with validation: +- [ ] `src/components/auth/LoginForm.tsx` + - Email + password fields + - react-hook-form + zod validation + - Loading states + - Error display + - Remember me checkbox (optional) +- [ ] `src/components/auth/RegisterForm.tsx` + - Email, password, first_name, last_name + - Password confirmation field + - Password strength indicator + - Validation matching backend rules: + - Min 8 chars + - 1 digit + - 1 uppercase letter + +Create pages: +- [ ] `src/app/(auth)/layout.tsx` - Centered form layout +- [ ] `src/app/(auth)/login/page.tsx` - Login page +- [ ] `src/app/(auth)/register/page.tsx` - Registration page + +**API Endpoints:** +- POST `/api/v1/auth/register` - Register new user +- POST `/api/v1/auth/login` - Authenticate user + +**Testing:** +- [ ] Form validation tests +- [ ] Submission success/error +- [ ] E2E login flow +- [ ] E2E registration flow +- [ ] Accessibility (keyboard nav, screen reader) + +**Reference:** `docs/COMPONENT_GUIDE.md` (form patterns), Requirements Section 8.1 + +### Task 2.5: Password Reset Flow 🔑 +**Status:** TODO 📋 +**Can run parallel with:** 2.3, 2.4 after 2.2 complete + +**Actions Needed:** + +Create password reset pages: +- [ ] `src/app/(auth)/password-reset/page.tsx` - Request reset +- [ ] `src/app/(auth)/password-reset/confirm/page.tsx` - Confirm reset with token + +Create forms: +- [ ] `src/components/auth/PasswordResetForm.tsx` - Email input form +- [ ] `src/components/auth/PasswordResetConfirmForm.tsx` - New password form + +**Flow:** +1. User enters email → POST `/api/v1/auth/password-reset/request` +2. User receives email with token link +3. User clicks link → Opens confirm page with token in URL +4. User enters new password → POST `/api/v1/auth/password-reset/confirm` + +**API Endpoints:** +- POST `/api/v1/auth/password-reset/request` - Request reset email +- POST `/api/v1/auth/password-reset/confirm` - Reset with token + +**Testing:** +- [ ] Request form validation +- [ ] Email sent confirmation message +- [ ] Token validation +- [ ] Password update success +- [ ] Expired token handling +- [ ] E2E password reset flow + +**Security Considerations:** +- [ ] Email enumeration protection (always show success) +- [ ] Token expiry handling +- [ ] Single-use tokens + +**Reference:** Requirements Section 4.3, `docs/FEATURE_EXAMPLES.md` + +### Phase 2 Review Checklist + +When Phase 2 is complete, verify: +- [ ] All auth pages functional +- [ ] Forms have proper validation +- [ ] Error messages are user-friendly +- [ ] Loading states on all async operations +- [ ] E2E tests for full auth flows pass +- [ ] Security audit completed +- [ ] Accessibility audit completed +- [ ] No console errors +- [ ] Works in mobile viewport +- [ ] Dark mode works on all pages + +**Before proceeding to Phase 3:** +- [ ] Run multi-agent review +- [ ] Security audit of auth implementation +- [ ] E2E test full auth flows +- [ ] Update this plan with actual progress + +--- + +## Phase 3: User Profile & Settings + +**Status:** TODO 📋 +**Duration:** 3-4 days +**Prerequisites:** Phase 2 complete + +**Detailed tasks will be added here after Phase 2 is complete.** + +**High-level Overview:** +- Authenticated layout with navigation +- User profile management +- Password change +- Session management UI +- User preferences (optional) + +--- + +## Phase 4-12: Future Phases + +**Status:** TODO 📋 + +**Remaining Phases:** +- **Phase 4:** Base Component Library & Layout +- **Phase 5:** Admin Dashboard Foundation +- **Phase 6:** User Management (Admin) +- **Phase 7:** Organization Management (Admin) +- **Phase 8:** Charts & Analytics +- **Phase 9:** Testing & Quality Assurance +- **Phase 10:** Documentation & Dev Tools +- **Phase 11:** Production Readiness & Optimization +- **Phase 12:** Final Integration & Handoff + +**Note:** These phases will be detailed in this document as we progress through each phase. Context from completed phases will inform the implementation of future phases. + +--- + +## Progress Tracking + +### Overall Progress Dashboard + +| Phase | Status | Started | Completed | Duration | Key Deliverables | +|-------|--------|---------|-----------|----------|------------------| +| 0: Foundation Docs | ✅ Complete | Oct 29 | Oct 29 | 1 day | 5 documentation files | +| 1: Infrastructure | ✅ Complete | Oct 29 | Oct 31 | 3 days | Setup + auth core + tests | +| 2: Auth System | 📋 TODO | - | - | 3-4 days | Login, register, reset flows | +| 3: User Settings | 📋 TODO | - | - | 3-4 days | Profile, password, sessions | +| 4: Component Library | 📋 TODO | - | - | 2-3 days | Common components | +| 5: Admin Foundation | 📋 TODO | - | - | 2-3 days | Admin layout, navigation | +| 6: User Management | 📋 TODO | - | - | 4-5 days | Admin user CRUD | +| 7: Org Management | 📋 TODO | - | - | 4-5 days | Admin org CRUD | +| 8: Charts | 📋 TODO | - | - | 2-3 days | Dashboard analytics | +| 9: Testing | 📋 TODO | - | - | 3-4 days | Comprehensive test suite | +| 10: Documentation | 📋 TODO | - | - | 2-3 days | Final docs | +| 11: Production Prep | 📋 TODO | - | - | 2-3 days | Performance, security | +| 12: Handoff | 📋 TODO | - | - | 1-2 days | Final validation | + +**Current:** Phase 1 Complete, Ready for Phase 2 +**Next:** Start Phase 2 - Authentication System UI + +### Task Status Legend +- ✅ **Complete** - Finished and reviewed +- ⚙ **In Progress** - Currently being worked on +- 📋 **TODO** - Not started +- ❌ **Blocked** - Cannot proceed due to dependencies +- 🔗 **Depends on** - Waiting for specific task + +--- + +## Critical Path & Dependencies + +### Sequential Dependencies (Must Complete in Order) + +1. **Phase 0** → Phase 1 (Foundation docs must exist before setup) +2. **Phase 1** → Phase 2 (Infrastructure needed for auth UI) +3. **Phase 2** → Phase 3 (Auth system needed for user features) +4. **Phase 1-4** → Phase 5 (Base components needed for admin) +5. **Phase 5** → Phase 6, 7 (Admin layout needed for CRUD) + +### Parallelization Opportunities + +**Within Phase 2 (After Task 2.2):** +- Tasks 2.3, 2.4, 2.5 can run in parallel (3 agents) + +**Within Phase 3 (After Task 3.1):** +- Tasks 3.2, 3.3, 3.4, 3.5 can run in parallel (4 agents) + +**Within Phase 4:** +- All tasks 4.1, 4.2, 4.3 can run in parallel (3 agents) + +**Within Phase 5 (After Task 5.1):** +- Tasks 5.2, 5.3, 5.4 can run in parallel (3 agents) + +**Phase 9 (Testing):** +- All testing tasks can run in parallel (4 agents) + +**Estimated Timeline:** +- **With 4 parallel agents:** 8-10 weeks +- **With 2 parallel agents:** 12-14 weeks +- **With 1 agent (sequential):** 18-20 weeks + +--- + +## Success Criteria + +### Template is Production-Ready When: + +1. ✅ All 12 phases complete +2. ✅ Test coverage ≥90% (unit + component + integration) +3. ✅ All E2E tests passing +4. ✅ Lighthouse scores: + - Performance >90 + - Accessibility 100 + - Best Practices >90 +5. ✅ WCAG 2.1 Level AA compliance verified +6. ✅ No high/critical security vulnerabilities +7. ✅ All documentation complete and accurate +8. ✅ Production deployment successful +9. ✅ Frontend-backend integration verified +10. ✅ Template can be extended by new developer using docs alone + +### Per-Phase Success Criteria + +**Each phase must meet these before proceeding:** +- [ ] All tasks complete +- [ ] Tests written and passing +- [ ] Code reviewed (self + multi-agent) +- [ ] Documentation updated +- [ ] No regressions in previous functionality +- [ ] This plan updated with actual progress + +--- + +## Critical Context for Resuming Work + +### If Conversation is Interrupted + +**To Resume Work, Read These Files in Order:** + +1. **THIS FILE** - `IMPLEMENTATION_PLAN.md` + - Current phase and progress + - What's been completed + - What's next + +2. **`frontend-requirements.md`** + - Complete feature requirements + - API endpoint reference + - User model details + +3. **`docs/ARCHITECTURE.md`** + - System design + - Technology stack + - Data flow patterns + +4. **`docs/CODING_STANDARDS.md`** + - Code style rules + - Testing standards + - Best practices + +5. **`docs/FEATURE_EXAMPLES.md`** + - Implementation patterns + - Code examples + - Common pitfalls + +### Key Commands Reference + +```bash +# Development +npm run dev # Start dev server (http://localhost:3000) +npm run build # Production build +npm run start # Start production server + +# Testing +npm test # Run tests +npm test -- --coverage # Run tests with coverage report +npm run type-check # TypeScript compilation check +npm run lint # ESLint check + +# API Client Generation (needs backend running) +npm run generate:api # Generate TypeScript client from OpenAPI spec + +# Package Management +npm install # Install dependencies +npm audit # Check for vulnerabilities +``` + +### Environment Variables + +**Required:** +```env +NEXT_PUBLIC_API_URL=http://localhost:8000 +NEXT_PUBLIC_APP_NAME=Template Project +``` + +**Optional:** +```env +NEXT_PUBLIC_API_TIMEOUT=30000 +NEXT_PUBLIC_TOKEN_REFRESH_THRESHOLD=300000 +NEXT_PUBLIC_DEBUG_API=false +``` + +See `.env.example` for complete list. + +### Current Technical State + +**What Works:** +- ✅ Authentication core (crypto, storage, store) +- ✅ Configuration management +- ✅ Test infrastructure +- ✅ TypeScript compilation +- ✅ Development environment + +**What's Needed Next:** +- [ ] Generate API client from backend +- [ ] Build auth UI (login, register, password reset) +- [ ] Implement auth pages +- [ ] Add E2E tests for auth flows + +**Technical Debt:** +- Manual API client files (will be replaced) +- Old implementation files (need cleanup) +- No API generation yet (needs backend) + +--- + +## References + +### Always Reference During Implementation + +**Primary Documents:** +- `IMPLEMENTATION_PLAN.md` (this file) - Implementation roadmap +- `frontend-requirements.md` - Detailed requirements +- `docs/ARCHITECTURE.md` - System design and patterns +- `docs/CODING_STANDARDS.md` - Code style and standards +- `docs/COMPONENT_GUIDE.md` - Component usage +- `docs/FEATURE_EXAMPLES.md` - Implementation examples +- `docs/API_INTEGRATION.md` - Backend API integration + +**Backend References:** +- `../backend/docs/ARCHITECTURE.md` - Backend patterns to mirror +- `../backend/docs/CODING_STANDARDS.md` - Backend conventions +- Backend OpenAPI spec: `http://localhost:8000/api/v1/openapi.json` + +**Testing References:** +- `jest.config.js` - Test configuration +- `jest.setup.js` - Global test setup +- `tests/` directory - Existing test patterns + +### Audit & Quality Reports + +**Available in `/tmp/`:** +- `AUDIT_SUMMARY.txt` - Quick reference +- `AUDIT_COMPLETE.md` - Full audit results +- `COVERAGE_CONFIG.md` - Coverage explanation +- `detailed_findings.md` - Issue analysis + +--- + +## Version History + +| Version | Date | Changes | Author | +|---------|------|---------|--------| +| 1.0 | Oct 29, 2025 | Initial plan created | Claude | +| 1.1 | Oct 31, 2025 | Phase 0 complete, updated structure | Claude | +| 1.2 | Oct 31, 2025 | Phase 1 complete, comprehensive audit | Claude | +| 1.3 | Oct 31, 2025 | **Major Update:** Reformatted as self-contained document | Claude | + +--- + +## Notes for Future Development + +### When Starting Phase 2 + +1. Generate API client first: + ```bash + # Ensure backend is running + cd ../backend && uvicorn app.main:app --reload + + # In separate terminal + cd frontend + npm run generate:api + ``` + +2. Review generated types in `src/lib/api/generated/` + +3. Replace manual client files: + - Archive or delete `src/lib/api/client.ts` + - Archive or delete `src/lib/api/errors.ts` + - Create thin wrapper if interceptor logic needed + +4. Follow patterns in `docs/FEATURE_EXAMPLES.md` + +5. Write tests alongside code (not after) + +### Remember + +- **Documentation First:** Check docs before implementing +- **Test As You Go:** Don't batch testing at end +- **Review Often:** Self-review after each task +- **Update This Plan:** Keep it current with actual progress +- **Context Matters:** This file + docs = full context + +--- + +**Last Updated:** October 31, 2025 +**Next Review:** After Phase 2 completion +**Contact:** Update this section with team contact info diff --git a/frontend/scripts/generate-api-client.sh b/frontend/scripts/generate-api-client.sh index 3d8cc9b..390676b 100755 --- a/frontend/scripts/generate-api-client.sh +++ b/frontend/scripts/generate-api-client.sh @@ -48,10 +48,7 @@ echo -e "${YELLOW}⚙️ Generating TypeScript API client...${NC}" if npx @hey-api/openapi-ts \ --input /tmp/openapi.json \ --output "$OUTPUT_DIR" \ - --client axios \ - --name ApiClient \ - --useOptions true \ - --exportSchemas true; then + --client @hey-api/client-axios; then echo -e "${GREEN}✓ API client generated successfully${NC}" else echo -e "${RED}✗ Failed to generate API client${NC}" diff --git a/frontend/src/lib/api/generated/client.gen.ts b/frontend/src/lib/api/generated/client.gen.ts new file mode 100644 index 0000000..cab3c70 --- /dev/null +++ b/frontend/src/lib/api/generated/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig()); diff --git a/frontend/src/lib/api/generated/client/client.gen.ts b/frontend/src/lib/api/generated/client/client.gen.ts new file mode 100644 index 0000000..f81a9e7 --- /dev/null +++ b/frontend/src/lib/api/generated/client/client.gen.ts @@ -0,0 +1,163 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + let instance: AxiosInstance; + + if (_config.axios && !('Axios' in _config.axios)) { + instance = _config.axios; + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { auth, ...configWithoutAuth } = _config; + instance = axios.create(configWithoutAuth); + } + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + instance.defaults = { + ...instance.defaults, + ..._config, + // @ts-expect-error + headers: mergeHeaders(instance.defaults.headers, _config.headers), + }; + return getConfig(); + }; + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + axios: options.axios ?? _config.axios ?? instance, + headers: mergeHeaders(_config.headers, options.headers), + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.body = opts.bodySerializer(opts.body); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + // @ts-expect-error + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + try { + // assign Axios here for consistency with fetch + const _axios = opts.axios!; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { auth, ...optsWithoutAuth } = opts; + const response = await _axios({ + ...optsWithoutAuth, + baseURL: '', // the baseURL is already included in `url` + data: getValidRequestBody(opts), + headers: opts.headers as RawAxiosRequestHeaders, + // let `paramsSerializer()` handle query params if it exists + params: opts.paramsSerializer ? opts.query : undefined, + url, + }); + + let { data } = response; + + if (opts.responseType === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return { + ...response, + data: data ?? {}, + }; + } catch (error) { + const e = error as AxiosError; + if (opts.throwOnError) { + throw e; + } + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; + } + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as Record, + method, + // @ts-expect-error + signal: opts.signal, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + instance, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/frontend/src/lib/api/generated/client/index.ts b/frontend/src/lib/api/generated/client/index.ts new file mode 100644 index 0000000..cff1d39 --- /dev/null +++ b/frontend/src/lib/api/generated/client/index.ts @@ -0,0 +1,24 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + OptionsLegacyParser, + RequestOptions, + RequestResult, + TDataShape, +} from './types.gen'; +export { createConfig } from './utils.gen'; diff --git a/frontend/src/lib/api/generated/client/types.gen.ts b/frontend/src/lib/api/generated/client/types.gen.ts new file mode 100644 index 0000000..d59239b --- /dev/null +++ b/frontend/src/lib/api/generated/client/types.gen.ts @@ -0,0 +1,216 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + AxiosError, + AxiosInstance, + AxiosRequestHeaders, + AxiosResponse, + AxiosStatic, + CreateAxiosDefaults, +} from 'axios'; + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; + +export interface Config + extends Omit, + CoreConfig { + /** + * Axios implementation. You can use this option to provide either an + * `AxiosStatic` or an `AxiosInstance`. + * + * @default axios + */ + axios?: AxiosStatic | AxiosInstance; + /** + * Base URL for all requests made by this client. + */ + baseURL?: T['baseURL']; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | AxiosRequestHeaders + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ClientOptions { + baseURL?: string; + throwOnError?: boolean; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, +> = ThrowOnError extends true + ? Promise< + AxiosResponse< + TData extends Record ? TData[keyof TData] : TData + > + > + : Promise< + | (AxiosResponse< + TData extends Record ? TData[keyof TData] : TData + > & { error: undefined }) + | (AxiosError< + TError extends Record ? TError[keyof TError] : TError + > & { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + }) + >; + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: Pick & Options, +) => string; + +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + instance: AxiosInstance; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + Omit; + +export type OptionsLegacyParser< + TData = unknown, + ThrowOnError extends boolean = boolean, +> = TData extends { body?: any } + ? TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys, 'body' | 'url'> & + TData & + Pick, 'headers'> + : TData extends { headers?: any } + ? OmitKeys, 'headers' | 'url'> & + TData & + Pick, 'body'> + : OmitKeys, 'url'> & TData; diff --git a/frontend/src/lib/api/generated/client/utils.gen.ts b/frontend/src/lib/api/generated/client/utils.gen.ts new file mode 100644 index 0000000..723c477 --- /dev/null +++ b/frontend/src/lib/api/generated/client/utils.gen.ts @@ -0,0 +1,213 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +const checkForExistence = ( + options: Pick & { + headers: Record; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if (name in options.headers || options.query?.[name]) { + return true; + } + if ( + 'Cookie' in options.headers && + options.headers['Cookie'] && + typeof options.headers['Cookie'] === 'string' + ) { + return options.headers['Cookie'].includes(`${name}=`); + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Record; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': { + const value = `${name}=${token}`; + if ('Cookie' in options.headers && options.headers['Cookie']) { + options.headers['Cookie'] = `${options.headers['Cookie']}; ${value}`; + } else { + options.headers['Cookie'] = value; + } + break; + } + case 'header': + default: + options.headers[name] = token; + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => { + const instanceBaseUrl = options.axios?.defaults?.baseURL; + + const baseUrl = + !!options.baseURL && typeof options.baseURL === 'string' + ? options.baseURL + : instanceBaseUrl; + + return getUrl({ + baseUrl: baseUrl as string, + path: options.path, + // let `paramsSerializer()` handle query params if it exists + query: !options.paramsSerializer ? options.query : undefined, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); +}; + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +/** + * Special Axios headers keywords allowing to set headers by request method. + */ +export const axiosHeadersKeywords = [ + 'common', + 'delete', + 'get', + 'head', + 'patch', + 'post', + 'put', +] as const; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Record => { + const mergedHeaders: Record = {}; + for (const header of headers) { + if (!header || typeof header !== 'object') { + continue; + } + + const iterator = Object.entries(header); + + for (const [key, value] of iterator) { + if ( + axiosHeadersKeywords.includes( + key as (typeof axiosHeadersKeywords)[number], + ) && + typeof value === 'object' + ) { + mergedHeaders[key] = { + ...(mergedHeaders[key] as Record), + ...value, + }; + } else if (value === null) { + delete mergedHeaders[key]; + } else if (Array.isArray(value)) { + for (const v of value) { + // @ts-expect-error + mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string]; + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders[key] = + typeof value === 'object' ? JSON.stringify(value) : (value as string); + } + } + } + return mergedHeaders; +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...override, +}); diff --git a/frontend/src/lib/api/generated/core/auth.gen.ts b/frontend/src/lib/api/generated/core/auth.gen.ts new file mode 100644 index 0000000..f8a7326 --- /dev/null +++ b/frontend/src/lib/api/generated/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/frontend/src/lib/api/generated/core/bodySerializer.gen.ts b/frontend/src/lib/api/generated/core/bodySerializer.gen.ts new file mode 100644 index 0000000..552b50f --- /dev/null +++ b/frontend/src/lib/api/generated/core/bodySerializer.gen.ts @@ -0,0 +1,100 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/frontend/src/lib/api/generated/core/params.gen.ts b/frontend/src/lib/api/generated/core/params.gen.ts new file mode 100644 index 0000000..71c88e8 --- /dev/null +++ b/frontend/src/lib/api/generated/core/params.gen.ts @@ -0,0 +1,153 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + { + in: Slot; + map?: string; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + (params[field.in] as Record)[name] = arg; + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else { + for (const [slot, allowed] of Object.entries( + config.allowExtra ?? {}, + )) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/frontend/src/lib/api/generated/core/pathSerializer.gen.ts b/frontend/src/lib/api/generated/core/pathSerializer.gen.ts new file mode 100644 index 0000000..8d99931 --- /dev/null +++ b/frontend/src/lib/api/generated/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/frontend/src/lib/api/generated/core/queryKeySerializer.gen.ts b/frontend/src/lib/api/generated/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000..d3bb683 --- /dev/null +++ b/frontend/src/lib/api/generated/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/frontend/src/lib/api/generated/core/serverSentEvents.gen.ts b/frontend/src/lib/api/generated/core/serverSentEvents.gen.ts new file mode 100644 index 0000000..f8fd78e --- /dev/null +++ b/frontend/src/lib/api/generated/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/frontend/src/lib/api/generated/core/types.gen.ts b/frontend/src/lib/api/generated/core/types.gen.ts new file mode 100644 index 0000000..643c070 --- /dev/null +++ b/frontend/src/lib/api/generated/core/types.gen.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/frontend/src/lib/api/generated/core/utils.gen.ts b/frontend/src/lib/api/generated/core/utils.gen.ts new file mode 100644 index 0000000..0b5389d --- /dev/null +++ b/frontend/src/lib/api/generated/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/frontend/src/lib/api/generated/index.ts b/frontend/src/lib/api/generated/index.ts old mode 100755 new mode 100644 index 7535e18..c352c10 --- a/frontend/src/lib/api/generated/index.ts +++ b/frontend/src/lib/api/generated/index.ts @@ -1,8 +1,4 @@ -// This file will be auto-generated by running: npm run generate:api -// Make sure the backend is running before generating the API client -// -// To generate: npm run generate:api -// -// This placeholder prevents import errors before generation +// This file is auto-generated by @hey-api/openapi-ts -export {} \ No newline at end of file +export type * from './types.gen'; +export * from './sdk.gen'; diff --git a/frontend/src/lib/api/generated/sdk.gen.ts b/frontend/src/lib/api/generated/sdk.gen.ts new file mode 100644 index 0000000..c97d746 --- /dev/null +++ b/frontend/src/lib/api/generated/sdk.gen.ts @@ -0,0 +1,913 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type Client, type Options as Options2, type TDataShape, urlSearchParamsBodySerializer } from './client'; +import { client } from './client.gen'; +import type { AdminActivateUserData, AdminActivateUserErrors, AdminActivateUserResponses, AdminAddOrganizationMemberData, AdminAddOrganizationMemberErrors, AdminAddOrganizationMemberResponses, AdminBulkUserActionData, AdminBulkUserActionErrors, AdminBulkUserActionResponses, AdminCreateOrganizationData, AdminCreateOrganizationErrors, AdminCreateOrganizationResponses, AdminCreateUserData, AdminCreateUserErrors, AdminCreateUserResponses, AdminDeactivateUserData, AdminDeactivateUserErrors, AdminDeactivateUserResponses, AdminDeleteOrganizationData, AdminDeleteOrganizationErrors, AdminDeleteOrganizationResponses, AdminDeleteUserData, AdminDeleteUserErrors, AdminDeleteUserResponses, AdminGetOrganizationData, AdminGetOrganizationErrors, AdminGetOrganizationResponses, AdminGetUserData, AdminGetUserErrors, AdminGetUserResponses, AdminListOrganizationMembersData, AdminListOrganizationMembersErrors, AdminListOrganizationMembersResponses, AdminListOrganizationsData, AdminListOrganizationsErrors, AdminListOrganizationsResponses, AdminListUsersData, AdminListUsersErrors, AdminListUsersResponses, AdminRemoveOrganizationMemberData, AdminRemoveOrganizationMemberErrors, AdminRemoveOrganizationMemberResponses, AdminUpdateOrganizationData, AdminUpdateOrganizationErrors, AdminUpdateOrganizationResponses, AdminUpdateUserData, AdminUpdateUserErrors, AdminUpdateUserResponses, ChangeCurrentUserPasswordData, ChangeCurrentUserPasswordErrors, ChangeCurrentUserPasswordResponses, CleanupExpiredSessionsData, CleanupExpiredSessionsResponses, ConfirmPasswordResetData, ConfirmPasswordResetErrors, ConfirmPasswordResetResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponses, GetCurrentUserInfoData, GetCurrentUserInfoResponses, GetCurrentUserProfileData, GetCurrentUserProfileResponses, GetMyOrganizationsData, GetMyOrganizationsErrors, GetMyOrganizationsResponses, GetOrganizationData, GetOrganizationErrors, GetOrganizationMembersData, GetOrganizationMembersErrors, GetOrganizationMembersResponses, GetOrganizationResponses, GetUserByIdData, GetUserByIdErrors, GetUserByIdResponses, HealthCheckData, HealthCheckResponses, ListMySessionsData, ListMySessionsResponses, ListUsersData, ListUsersErrors, ListUsersResponses, LoginData, LoginErrors, LoginOauthData, LoginOauthErrors, LoginOauthResponses, LoginResponses, LogoutAllData, LogoutAllResponses, LogoutData, LogoutErrors, LogoutResponses, RefreshTokenData, RefreshTokenErrors, RefreshTokenResponses, RegisterData, RegisterErrors, RegisterResponses, RequestPasswordResetData, RequestPasswordResetErrors, RequestPasswordResetResponses, RevokeSessionData, RevokeSessionErrors, RevokeSessionResponses, RootGetData, RootGetResponses, UpdateCurrentUserData, UpdateCurrentUserErrors, UpdateCurrentUserResponses, UpdateOrganizationData, UpdateOrganizationErrors, UpdateOrganizationResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses } from './types.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +/** + * Root + */ +export const rootGet = (options?: Options) => { + return (options?.client ?? client).get({ + responseType: 'text', + url: '/', + ...options + }); +}; + +/** + * Health Check + * + * Check the health status of the API and its dependencies + */ +export const healthCheck = (options?: Options) => { + return (options?.client ?? client).get({ + responseType: 'json', + url: '/health', + ...options + }); +}; + +/** + * Register User + * + * Register a new user. + * + * Returns: + * The created user information. + */ +export const register = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + url: '/api/v1/auth/register', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Login + * + * Login with username and password. + * + * Creates a new session for this device. + * + * Returns: + * Access and refresh tokens. + */ +export const login = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + url: '/api/v1/auth/login', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Login Oauth + * + * OAuth2-compatible login endpoint, used by the OpenAPI UI. + * + * Creates a new session for this device. + * + * Returns: + * Access and refresh tokens. + */ +export const loginOauth = (options: Options) => { + return (options.client ?? client).post({ + ...urlSearchParamsBodySerializer, + responseType: 'json', + url: '/api/v1/auth/login/oauth', + ...options, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + ...options.headers + } + }); +}; + +/** + * Refresh Token + * + * Refresh access token using a refresh token. + * + * Validates that the session is still active before issuing new tokens. + * + * Returns: + * New access and refresh tokens. + */ +export const refreshToken = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + url: '/api/v1/auth/refresh', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Get Current User Info + * + * Get current user information. + * + * Requires authentication. + */ +export const getCurrentUserInfo = (options?: Options) => { + return (options?.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/auth/me', + ...options + }); +}; + +/** + * Request Password Reset + * + * Request a password reset link. + * + * An email will be sent with a reset link if the email exists. + * Always returns success to prevent email enumeration. + * + * **Rate Limit**: 3 requests/minute + */ +export const requestPasswordReset = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + url: '/api/v1/auth/password-reset/request', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Confirm Password Reset + * + * Reset password using a token from email. + * + * **Rate Limit**: 5 requests/minute + */ +export const confirmPasswordReset = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + url: '/api/v1/auth/password-reset/confirm', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Logout from Current Device + * + * Logout from the current device only. + * + * Other devices will remain logged in. + * + * Requires the refresh token to identify which session to terminate. + * + * **Rate Limit**: 10 requests/minute + */ +export const logout = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/auth/logout', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Logout from All Devices + * + * Logout from ALL devices. + * + * This will terminate all active sessions for the current user. + * You will need to log in again on all devices. + * + * **Rate Limit**: 5 requests/minute + */ +export const logoutAll = (options?: Options) => { + return (options?.client ?? client).post({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/auth/logout-all', + ...options + }); +}; + +/** + * List Users + * + * List all users with pagination, filtering, and sorting (admin only). + * + * **Authentication**: Required (Bearer token) + * **Authorization**: Superuser only + * + * **Filtering**: is_active, is_superuser + * **Sorting**: Any user field (email, first_name, last_name, created_at, etc.) + * + * **Rate Limit**: 60 requests/minute + */ +export const listUsers = (options?: Options) => { + return (options?.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/users', + ...options + }); +}; + +/** + * Get Current User + * + * Get the current authenticated user's profile. + * + * **Authentication**: Required (Bearer token) + * + * **Rate Limit**: 60 requests/minute + */ +export const getCurrentUserProfile = (options?: Options) => { + return (options?.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/users/me', + ...options + }); +}; + +/** + * Update Current User + * + * Update the current authenticated user's profile. + * + * Users can update their own profile information (except is_superuser). + * + * **Authentication**: Required (Bearer token) + * + * **Rate Limit**: 30 requests/minute + */ +export const updateCurrentUser = (options: Options) => { + return (options.client ?? client).patch({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/users/me', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Delete User + * + * Delete a specific user by their ID. + * + * **Authentication**: Required (Bearer token) + * **Authorization**: Superuser only + * + * **Rate Limit**: 10 requests/minute + * + * **Note**: This performs a hard delete. Consider implementing soft deletes for production. + */ +export const deleteUser = (options: Options) => { + return (options.client ?? client).delete({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/users/{user_id}', + ...options + }); +}; + +/** + * Get User by ID + * + * Get a specific user by their ID. + * + * **Authentication**: Required (Bearer token) + * **Authorization**: + * - Regular users: Can only access their own profile + * - Superusers: Can access any profile + * + * **Rate Limit**: 60 requests/minute + */ +export const getUserById = (options: Options) => { + return (options.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/users/{user_id}', + ...options + }); +}; + +/** + * Update User + * + * Update a specific user by their ID. + * + * **Authentication**: Required (Bearer token) + * **Authorization**: + * - Regular users: Can only update their own profile (except is_superuser) + * - Superusers: Can update any profile + * + * **Rate Limit**: 30 requests/minute + */ +export const updateUser = (options: Options) => { + return (options.client ?? client).patch({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/users/{user_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Change Current User Password + * + * Change the current authenticated user's password. + * + * Requires the current password for verification. + * + * **Authentication**: Required (Bearer token) + * + * **Rate Limit**: 5 requests/minute + */ +export const changeCurrentUserPassword = (options: Options) => { + return (options.client ?? client).patch({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/users/me/password', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * List My Active Sessions + * + * Get a list of all active sessions for the current user. + * + * This shows where you're currently logged in. + * + * **Rate Limit**: 30 requests/minute + */ +export const listMySessions = (options?: Options) => { + return (options?.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/sessions/me', + ...options + }); +}; + +/** + * Revoke Specific Session + * + * Revoke a specific session by ID. + * + * This logs you out from that particular device. + * You can only revoke your own sessions. + * + * **Rate Limit**: 10 requests/minute + */ +export const revokeSession = (options: Options) => { + return (options.client ?? client).delete({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/sessions/{session_id}', + ...options + }); +}; + +/** + * Cleanup Expired Sessions + * + * Remove expired sessions for the current user. + * + * This is a cleanup operation to remove old session records. + * + * **Rate Limit**: 5 requests/minute + */ +export const cleanupExpiredSessions = (options?: Options) => { + return (options?.client ?? client).delete({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/sessions/me/expired', + ...options + }); +}; + +/** + * Admin: List All Users + * + * Get paginated list of all users with filtering and search (admin only) + */ +export const adminListUsers = (options?: Options) => { + return (options?.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/users', + ...options + }); +}; + +/** + * Admin: Create User + * + * Create a new user (admin only) + */ +export const adminCreateUser = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/users', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Admin: Delete User + * + * Soft delete a user (admin only) + */ +export const adminDeleteUser = (options: Options) => { + return (options.client ?? client).delete({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/users/{user_id}', + ...options + }); +}; + +/** + * Admin: Get User Details + * + * Get detailed user information (admin only) + */ +export const adminGetUser = (options: Options) => { + return (options.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/users/{user_id}', + ...options + }); +}; + +/** + * Admin: Update User + * + * Update user information (admin only) + */ +export const adminUpdateUser = (options: Options) => { + return (options.client ?? client).put({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/users/{user_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Admin: Activate User + * + * Activate a user account (admin only) + */ +export const adminActivateUser = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/users/{user_id}/activate', + ...options + }); +}; + +/** + * Admin: Deactivate User + * + * Deactivate a user account (admin only) + */ +export const adminDeactivateUser = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/users/{user_id}/deactivate', + ...options + }); +}; + +/** + * Admin: Bulk User Action + * + * Perform bulk actions on multiple users (admin only) + */ +export const adminBulkUserAction = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/users/bulk-action', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Admin: List Organizations + * + * Get paginated list of all organizations (admin only) + */ +export const adminListOrganizations = (options?: Options) => { + return (options?.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/organizations', + ...options + }); +}; + +/** + * Admin: Create Organization + * + * Create a new organization (admin only) + */ +export const adminCreateOrganization = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/organizations', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Admin: Delete Organization + * + * Delete an organization (admin only) + */ +export const adminDeleteOrganization = (options: Options) => { + return (options.client ?? client).delete({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/organizations/{org_id}', + ...options + }); +}; + +/** + * Admin: Get Organization Details + * + * Get detailed organization information (admin only) + */ +export const adminGetOrganization = (options: Options) => { + return (options.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/organizations/{org_id}', + ...options + }); +}; + +/** + * Admin: Update Organization + * + * Update organization information (admin only) + */ +export const adminUpdateOrganization = (options: Options) => { + return (options.client ?? client).put({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/organizations/{org_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Admin: List Organization Members + * + * Get all members of an organization (admin only) + */ +export const adminListOrganizationMembers = (options: Options) => { + return (options.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/organizations/{org_id}/members', + ...options + }); +}; + +/** + * Admin: Add Member to Organization + * + * Add a user to an organization (admin only) + */ +export const adminAddOrganizationMember = (options: Options) => { + return (options.client ?? client).post({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/organizations/{org_id}/members', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Admin: Remove Member from Organization + * + * Remove a user from an organization (admin only) + */ +export const adminRemoveOrganizationMember = (options: Options) => { + return (options.client ?? client).delete({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/admin/organizations/{org_id}/members/{user_id}', + ...options + }); +}; + +/** + * Get My Organizations + * + * Get all organizations the current user belongs to + */ +export const getMyOrganizations = (options?: Options) => { + return (options?.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/organizations/me', + ...options + }); +}; + +/** + * Get Organization Details + * + * Get details of an organization the user belongs to + */ +export const getOrganization = (options: Options) => { + return (options.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/organizations/{organization_id}', + ...options + }); +}; + +/** + * Update Organization + * + * Update organization details (admin/owner only) + */ +export const updateOrganization = (options: Options) => { + return (options.client ?? client).put({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/organizations/{organization_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Get Organization Members + * + * Get all members of an organization (members can view) + */ +export const getOrganizationMembers = (options: Options) => { + return (options.client ?? client).get({ + responseType: 'json', + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/organizations/{organization_id}/members', + ...options + }); +}; diff --git a/frontend/src/lib/api/generated/types.gen.ts b/frontend/src/lib/api/generated/types.gen.ts new file mode 100644 index 0000000..800c0e1 --- /dev/null +++ b/frontend/src/lib/api/generated/types.gen.ts @@ -0,0 +1,1853 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseURL: `${string}://${string}` | (string & {}); +}; + +/** + * AddMemberRequest + * + * Request to add a member to an organization. + */ +export type AddMemberRequest = { + /** + * User Id + * + * User ID to add + */ + user_id: string; + /** + * Role in organization + */ + role?: OrganizationRole; +}; + +/** + * Body_login_oauth + */ +export type BodyLoginOauth = { + /** + * Grant Type + */ + grant_type?: string | null; + /** + * Username + */ + username: string; + /** + * Password + */ + password: string; + /** + * Scope + */ + scope?: string; + /** + * Client Id + */ + client_id?: string | null; + /** + * Client Secret + */ + client_secret?: string | null; +}; + +/** + * BulkAction + * + * Supported bulk actions. + */ +export type BulkAction = 'activate' | 'deactivate' | 'delete'; + +/** + * BulkActionResult + * + * Result of a bulk action. + */ +export type BulkActionResult = { + /** + * Success + */ + success: boolean; + /** + * Affected Count + */ + affected_count: number; + /** + * Failed Count + */ + failed_count: number; + /** + * Message + */ + message: string; + /** + * Failed Ids + */ + failed_ids?: Array | null; +}; + +/** + * BulkUserAction + * + * Schema for bulk user actions. + */ +export type BulkUserAction = { + /** + * Action to perform on selected users + */ + action: BulkAction; + /** + * User Ids + * + * List of user IDs (max 100) + */ + user_ids: Array; +}; + +/** + * HTTPValidationError + */ +export type HttpValidationError = { + /** + * Detail + */ + detail?: Array; +}; + +/** + * LoginRequest + */ +export type LoginRequest = { + /** + * Email + */ + email: string; + /** + * Password + */ + password: string; +}; + +/** + * LogoutRequest + * + * Request schema for logout endpoint. + */ +export type LogoutRequest = { + /** + * Refresh Token + * + * Refresh token for the session to logout from + */ + refresh_token: string; +}; + +/** + * MessageResponse + * + * Simple message response. + */ +export type MessageResponse = { + /** + * Success + * + * Operation success status + */ + success?: boolean; + /** + * Message + * + * Human-readable message + */ + message: string; +}; + +/** + * OrganizationCreate + * + * Schema for creating a new organization. + */ +export type OrganizationCreate = { + /** + * Name + */ + name: string; + /** + * Slug + */ + slug: string; + /** + * Description + */ + description?: string | null; + /** + * Is Active + */ + is_active?: boolean; + /** + * Settings + */ + settings?: { + [key: string]: unknown; + } | null; +}; + +/** + * OrganizationMemberResponse + * + * Schema for organization member information. + */ +export type OrganizationMemberResponse = { + /** + * User Id + */ + user_id: string; + /** + * Email + */ + email: string; + /** + * First Name + */ + first_name: string; + /** + * Last Name + */ + last_name?: string | null; + role: OrganizationRole; + /** + * Is Active + */ + is_active: boolean; + /** + * Joined At + */ + joined_at: string; +}; + +/** + * OrganizationResponse + * + * Schema for organization API responses. + */ +export type OrganizationResponse = { + /** + * Name + */ + name: string; + /** + * Slug + */ + slug?: string | null; + /** + * Description + */ + description?: string | null; + /** + * Is Active + */ + is_active?: boolean; + /** + * Settings + */ + settings?: { + [key: string]: unknown; + } | null; + /** + * Id + */ + id: string; + /** + * Created At + */ + created_at: string; + /** + * Updated At + */ + updated_at?: string | null; + /** + * Member Count + */ + member_count?: number | null; +}; + +/** + * OrganizationRole + * + * Built-in organization roles. + * These provide a baseline role system that can be optionally used. + * Projects can extend this or implement their own permission system. + */ +export type OrganizationRole = 'owner' | 'admin' | 'member' | 'guest'; + +/** + * OrganizationUpdate + * + * Schema for updating an organization. + */ +export type OrganizationUpdate = { + /** + * Name + */ + name?: string | null; + /** + * Slug + */ + slug?: string | null; + /** + * Description + */ + description?: string | null; + /** + * Is Active + */ + is_active?: boolean | null; + /** + * Settings + */ + settings?: { + [key: string]: unknown; + } | null; +}; + +/** + * PaginatedResponse[OrganizationMemberResponse] + */ +export type PaginatedResponseOrganizationMemberResponse = { + /** + * Data + * + * List of items + */ + data: Array; + /** + * Pagination metadata + */ + pagination: PaginationMeta; +}; + +/** + * PaginatedResponse[OrganizationResponse] + */ +export type PaginatedResponseOrganizationResponse = { + /** + * Data + * + * List of items + */ + data: Array; + /** + * Pagination metadata + */ + pagination: PaginationMeta; +}; + +/** + * PaginatedResponse[UserResponse] + */ +export type PaginatedResponseUserResponse = { + /** + * Data + * + * List of items + */ + data: Array; + /** + * Pagination metadata + */ + pagination: PaginationMeta; +}; + +/** + * PaginationMeta + * + * Metadata for paginated responses. + */ +export type PaginationMeta = { + /** + * Total + * + * Total number of items + */ + total: number; + /** + * Page + * + * Current page number + */ + page: number; + /** + * Page Size + * + * Number of items in current page + */ + page_size: number; + /** + * Total Pages + * + * Total number of pages + */ + total_pages: number; + /** + * Has Next + * + * Whether there is a next page + */ + has_next: boolean; + /** + * Has Prev + * + * Whether there is a previous page + */ + has_prev: boolean; +}; + +/** + * PasswordChange + * + * Schema for changing password (requires current password). + */ +export type PasswordChange = { + /** + * Current Password + */ + current_password: string; + /** + * New Password + */ + new_password: string; +}; + +/** + * PasswordResetConfirm + * + * Schema for confirming a password reset with token. + */ +export type PasswordResetConfirm = { + /** + * Token + * + * Password reset token from email + */ + token: string; + /** + * New Password + * + * New password + */ + new_password: string; +}; + +/** + * PasswordResetRequest + * + * Schema for requesting a password reset. + */ +export type PasswordResetRequest = { + /** + * Email + * + * Email address of the account + */ + email: string; +}; + +/** + * RefreshTokenRequest + */ +export type RefreshTokenRequest = { + /** + * Refresh Token + */ + refresh_token: string; +}; + +/** + * SessionListResponse + * + * Response containing list of sessions. + */ +export type SessionListResponse = { + /** + * Sessions + */ + sessions: Array; + /** + * Total + * + * Total number of active sessions + */ + total: number; +}; + +/** + * SessionResponse + * + * Schema for session responses to clients. + * + * This is what users see when they list their active sessions. + */ +export type SessionResponse = { + /** + * Device Name + * + * Friendly device name + */ + device_name?: string | null; + /** + * Device Id + * + * Persistent device identifier + */ + device_id?: string | null; + /** + * Id + */ + id: string; + /** + * Ip Address + */ + ip_address?: string | null; + /** + * Location City + */ + location_city?: string | null; + /** + * Location Country + */ + location_country?: string | null; + /** + * Last Used At + */ + last_used_at: string; + /** + * Created At + */ + created_at: string; + /** + * Expires At + */ + expires_at: string; + /** + * Is Current + * + * Whether this is the current session + */ + is_current?: boolean; +}; + +/** + * SortOrder + * + * Sort order options. + */ +export type SortOrder = 'asc' | 'desc'; + +/** + * Token + */ +export type Token = { + /** + * Access Token + */ + access_token: string; + /** + * Refresh Token + */ + refresh_token?: string | null; + /** + * Token Type + */ + token_type?: string; +}; + +/** + * UserCreate + */ +export type UserCreate = { + /** + * Email + */ + email: string; + /** + * First Name + */ + first_name: string; + /** + * Last Name + */ + last_name?: string | null; + /** + * Phone Number + */ + phone_number?: string | null; + /** + * Password + */ + password: string; + /** + * Is Superuser + */ + is_superuser?: boolean; +}; + +/** + * UserResponse + */ +export type UserResponse = { + /** + * Email + */ + email: string; + /** + * First Name + */ + first_name: string; + /** + * Last Name + */ + last_name?: string | null; + /** + * Phone Number + */ + phone_number?: string | null; + /** + * Id + */ + id: string; + /** + * Is Active + */ + is_active: boolean; + /** + * Is Superuser + */ + is_superuser: boolean; + /** + * Created At + */ + created_at: string; + /** + * Updated At + */ + updated_at?: string | null; +}; + +/** + * UserUpdate + */ +export type UserUpdate = { + /** + * First Name + */ + first_name?: string | null; + /** + * Last Name + */ + last_name?: string | null; + /** + * Phone Number + */ + phone_number?: string | null; + /** + * Preferences + */ + preferences?: { + [key: string]: unknown; + } | null; + /** + * Is Active + */ + is_active?: boolean | null; +}; + +/** + * ValidationError + */ +export type ValidationError = { + /** + * Location + */ + loc: Array; + /** + * Message + */ + msg: string; + /** + * Error Type + */ + type: string; +}; + +export type RootGetData = { + body?: never; + path?: never; + query?: never; + url: '/'; +}; + +export type RootGetResponses = { + /** + * Successful Response + */ + 200: string; +}; + +export type RootGetResponse = RootGetResponses[keyof RootGetResponses]; + +export type HealthCheckData = { + body?: never; + path?: never; + query?: never; + url: '/health'; +}; + +export type HealthCheckResponses = { + /** + * Health status information + */ + 200: unknown; +}; + +export type RegisterData = { + body: UserCreate; + path?: never; + query?: never; + url: '/api/v1/auth/register'; +}; + +export type RegisterErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type RegisterError = RegisterErrors[keyof RegisterErrors]; + +export type RegisterResponses = { + /** + * Successful Response + */ + 201: UserResponse; +}; + +export type RegisterResponse = RegisterResponses[keyof RegisterResponses]; + +export type LoginData = { + body: LoginRequest; + path?: never; + query?: never; + url: '/api/v1/auth/login'; +}; + +export type LoginErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type LoginError = LoginErrors[keyof LoginErrors]; + +export type LoginResponses = { + /** + * Successful Response + */ + 200: Token; +}; + +export type LoginResponse = LoginResponses[keyof LoginResponses]; + +export type LoginOauthData = { + body: BodyLoginOauth; + path?: never; + query?: never; + url: '/api/v1/auth/login/oauth'; +}; + +export type LoginOauthErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type LoginOauthError = LoginOauthErrors[keyof LoginOauthErrors]; + +export type LoginOauthResponses = { + /** + * Successful Response + */ + 200: Token; +}; + +export type LoginOauthResponse = LoginOauthResponses[keyof LoginOauthResponses]; + +export type RefreshTokenData = { + body: RefreshTokenRequest; + path?: never; + query?: never; + url: '/api/v1/auth/refresh'; +}; + +export type RefreshTokenErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type RefreshTokenError = RefreshTokenErrors[keyof RefreshTokenErrors]; + +export type RefreshTokenResponses = { + /** + * Successful Response + */ + 200: Token; +}; + +export type RefreshTokenResponse = RefreshTokenResponses[keyof RefreshTokenResponses]; + +export type GetCurrentUserInfoData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/me'; +}; + +export type GetCurrentUserInfoResponses = { + /** + * Successful Response + */ + 200: UserResponse; +}; + +export type GetCurrentUserInfoResponse = GetCurrentUserInfoResponses[keyof GetCurrentUserInfoResponses]; + +export type RequestPasswordResetData = { + body: PasswordResetRequest; + path?: never; + query?: never; + url: '/api/v1/auth/password-reset/request'; +}; + +export type RequestPasswordResetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type RequestPasswordResetError = RequestPasswordResetErrors[keyof RequestPasswordResetErrors]; + +export type RequestPasswordResetResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type RequestPasswordResetResponse = RequestPasswordResetResponses[keyof RequestPasswordResetResponses]; + +export type ConfirmPasswordResetData = { + body: PasswordResetConfirm; + path?: never; + query?: never; + url: '/api/v1/auth/password-reset/confirm'; +}; + +export type ConfirmPasswordResetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ConfirmPasswordResetError = ConfirmPasswordResetErrors[keyof ConfirmPasswordResetErrors]; + +export type ConfirmPasswordResetResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type ConfirmPasswordResetResponse = ConfirmPasswordResetResponses[keyof ConfirmPasswordResetResponses]; + +export type LogoutData = { + body: LogoutRequest; + path?: never; + query?: never; + url: '/api/v1/auth/logout'; +}; + +export type LogoutErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type LogoutError = LogoutErrors[keyof LogoutErrors]; + +export type LogoutResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type LogoutResponse = LogoutResponses[keyof LogoutResponses]; + +export type LogoutAllData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/logout-all'; +}; + +export type LogoutAllResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type LogoutAllResponse = LogoutAllResponses[keyof LogoutAllResponses]; + +export type ListUsersData = { + body?: never; + path?: never; + query?: { + /** + * Is Active + * + * Filter by active status + */ + is_active?: boolean | null; + /** + * Is Superuser + * + * Filter by superuser status + */ + is_superuser?: boolean | null; + /** + * Page + */ + page?: number; + /** + * Limit + */ + limit?: number; + /** + * Sort By + */ + sort_by?: string | null; + sort_order?: SortOrder; + }; + url: '/api/v1/users'; +}; + +export type ListUsersErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ListUsersError = ListUsersErrors[keyof ListUsersErrors]; + +export type ListUsersResponses = { + /** + * Successful Response + */ + 200: PaginatedResponseUserResponse; +}; + +export type ListUsersResponse = ListUsersResponses[keyof ListUsersResponses]; + +export type GetCurrentUserProfileData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/users/me'; +}; + +export type GetCurrentUserProfileResponses = { + /** + * Successful Response + */ + 200: UserResponse; +}; + +export type GetCurrentUserProfileResponse = GetCurrentUserProfileResponses[keyof GetCurrentUserProfileResponses]; + +export type UpdateCurrentUserData = { + body: UserUpdate; + path?: never; + query?: never; + url: '/api/v1/users/me'; +}; + +export type UpdateCurrentUserErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateCurrentUserError = UpdateCurrentUserErrors[keyof UpdateCurrentUserErrors]; + +export type UpdateCurrentUserResponses = { + /** + * Successful Response + */ + 200: UserResponse; +}; + +export type UpdateCurrentUserResponse = UpdateCurrentUserResponses[keyof UpdateCurrentUserResponses]; + +export type DeleteUserData = { + body?: never; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/users/{user_id}'; +}; + +export type DeleteUserErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteUserError = DeleteUserErrors[keyof DeleteUserErrors]; + +export type DeleteUserResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type DeleteUserResponse = DeleteUserResponses[keyof DeleteUserResponses]; + +export type GetUserByIdData = { + body?: never; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/users/{user_id}'; +}; + +export type GetUserByIdErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetUserByIdError = GetUserByIdErrors[keyof GetUserByIdErrors]; + +export type GetUserByIdResponses = { + /** + * Successful Response + */ + 200: UserResponse; +}; + +export type GetUserByIdResponse = GetUserByIdResponses[keyof GetUserByIdResponses]; + +export type UpdateUserData = { + body: UserUpdate; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/users/{user_id}'; +}; + +export type UpdateUserErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateUserError = UpdateUserErrors[keyof UpdateUserErrors]; + +export type UpdateUserResponses = { + /** + * Successful Response + */ + 200: UserResponse; +}; + +export type UpdateUserResponse = UpdateUserResponses[keyof UpdateUserResponses]; + +export type ChangeCurrentUserPasswordData = { + body: PasswordChange; + path?: never; + query?: never; + url: '/api/v1/users/me/password'; +}; + +export type ChangeCurrentUserPasswordErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ChangeCurrentUserPasswordError = ChangeCurrentUserPasswordErrors[keyof ChangeCurrentUserPasswordErrors]; + +export type ChangeCurrentUserPasswordResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type ChangeCurrentUserPasswordResponse = ChangeCurrentUserPasswordResponses[keyof ChangeCurrentUserPasswordResponses]; + +export type ListMySessionsData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/sessions/me'; +}; + +export type ListMySessionsResponses = { + /** + * Successful Response + */ + 200: SessionListResponse; +}; + +export type ListMySessionsResponse = ListMySessionsResponses[keyof ListMySessionsResponses]; + +export type RevokeSessionData = { + body?: never; + path: { + /** + * Session Id + */ + session_id: string; + }; + query?: never; + url: '/api/v1/sessions/{session_id}'; +}; + +export type RevokeSessionErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type RevokeSessionError = RevokeSessionErrors[keyof RevokeSessionErrors]; + +export type RevokeSessionResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type RevokeSessionResponse = RevokeSessionResponses[keyof RevokeSessionResponses]; + +export type CleanupExpiredSessionsData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/sessions/me/expired'; +}; + +export type CleanupExpiredSessionsResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type CleanupExpiredSessionsResponse = CleanupExpiredSessionsResponses[keyof CleanupExpiredSessionsResponses]; + +export type AdminListUsersData = { + body?: never; + path?: never; + query?: { + /** + * Is Active + * + * Filter by active status + */ + is_active?: boolean | null; + /** + * Is Superuser + * + * Filter by superuser status + */ + is_superuser?: boolean | null; + /** + * Search + * + * Search by email, name + */ + search?: string | null; + /** + * Page + */ + page?: number; + /** + * Limit + */ + limit?: number; + /** + * Sort By + */ + sort_by?: string | null; + sort_order?: SortOrder; + }; + url: '/api/v1/admin/users'; +}; + +export type AdminListUsersErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminListUsersError = AdminListUsersErrors[keyof AdminListUsersErrors]; + +export type AdminListUsersResponses = { + /** + * Successful Response + */ + 200: PaginatedResponseUserResponse; +}; + +export type AdminListUsersResponse = AdminListUsersResponses[keyof AdminListUsersResponses]; + +export type AdminCreateUserData = { + body: UserCreate; + path?: never; + query?: never; + url: '/api/v1/admin/users'; +}; + +export type AdminCreateUserErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminCreateUserError = AdminCreateUserErrors[keyof AdminCreateUserErrors]; + +export type AdminCreateUserResponses = { + /** + * Successful Response + */ + 201: UserResponse; +}; + +export type AdminCreateUserResponse = AdminCreateUserResponses[keyof AdminCreateUserResponses]; + +export type AdminDeleteUserData = { + body?: never; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/admin/users/{user_id}'; +}; + +export type AdminDeleteUserErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminDeleteUserError = AdminDeleteUserErrors[keyof AdminDeleteUserErrors]; + +export type AdminDeleteUserResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type AdminDeleteUserResponse = AdminDeleteUserResponses[keyof AdminDeleteUserResponses]; + +export type AdminGetUserData = { + body?: never; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/admin/users/{user_id}'; +}; + +export type AdminGetUserErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminGetUserError = AdminGetUserErrors[keyof AdminGetUserErrors]; + +export type AdminGetUserResponses = { + /** + * Successful Response + */ + 200: UserResponse; +}; + +export type AdminGetUserResponse = AdminGetUserResponses[keyof AdminGetUserResponses]; + +export type AdminUpdateUserData = { + body: UserUpdate; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/admin/users/{user_id}'; +}; + +export type AdminUpdateUserErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminUpdateUserError = AdminUpdateUserErrors[keyof AdminUpdateUserErrors]; + +export type AdminUpdateUserResponses = { + /** + * Successful Response + */ + 200: UserResponse; +}; + +export type AdminUpdateUserResponse = AdminUpdateUserResponses[keyof AdminUpdateUserResponses]; + +export type AdminActivateUserData = { + body?: never; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/admin/users/{user_id}/activate'; +}; + +export type AdminActivateUserErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminActivateUserError = AdminActivateUserErrors[keyof AdminActivateUserErrors]; + +export type AdminActivateUserResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type AdminActivateUserResponse = AdminActivateUserResponses[keyof AdminActivateUserResponses]; + +export type AdminDeactivateUserData = { + body?: never; + path: { + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/admin/users/{user_id}/deactivate'; +}; + +export type AdminDeactivateUserErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminDeactivateUserError = AdminDeactivateUserErrors[keyof AdminDeactivateUserErrors]; + +export type AdminDeactivateUserResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type AdminDeactivateUserResponse = AdminDeactivateUserResponses[keyof AdminDeactivateUserResponses]; + +export type AdminBulkUserActionData = { + body: BulkUserAction; + path?: never; + query?: never; + url: '/api/v1/admin/users/bulk-action'; +}; + +export type AdminBulkUserActionErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminBulkUserActionError = AdminBulkUserActionErrors[keyof AdminBulkUserActionErrors]; + +export type AdminBulkUserActionResponses = { + /** + * Successful Response + */ + 200: BulkActionResult; +}; + +export type AdminBulkUserActionResponse = AdminBulkUserActionResponses[keyof AdminBulkUserActionResponses]; + +export type AdminListOrganizationsData = { + body?: never; + path?: never; + query?: { + /** + * Is Active + * + * Filter by active status + */ + is_active?: boolean | null; + /** + * Search + * + * Search by name, slug, description + */ + search?: string | null; + /** + * Page + */ + page?: number; + /** + * Limit + */ + limit?: number; + }; + url: '/api/v1/admin/organizations'; +}; + +export type AdminListOrganizationsErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminListOrganizationsError = AdminListOrganizationsErrors[keyof AdminListOrganizationsErrors]; + +export type AdminListOrganizationsResponses = { + /** + * Successful Response + */ + 200: PaginatedResponseOrganizationResponse; +}; + +export type AdminListOrganizationsResponse = AdminListOrganizationsResponses[keyof AdminListOrganizationsResponses]; + +export type AdminCreateOrganizationData = { + body: OrganizationCreate; + path?: never; + query?: never; + url: '/api/v1/admin/organizations'; +}; + +export type AdminCreateOrganizationErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminCreateOrganizationError = AdminCreateOrganizationErrors[keyof AdminCreateOrganizationErrors]; + +export type AdminCreateOrganizationResponses = { + /** + * Successful Response + */ + 201: OrganizationResponse; +}; + +export type AdminCreateOrganizationResponse = AdminCreateOrganizationResponses[keyof AdminCreateOrganizationResponses]; + +export type AdminDeleteOrganizationData = { + body?: never; + path: { + /** + * Org Id + */ + org_id: string; + }; + query?: never; + url: '/api/v1/admin/organizations/{org_id}'; +}; + +export type AdminDeleteOrganizationErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminDeleteOrganizationError = AdminDeleteOrganizationErrors[keyof AdminDeleteOrganizationErrors]; + +export type AdminDeleteOrganizationResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type AdminDeleteOrganizationResponse = AdminDeleteOrganizationResponses[keyof AdminDeleteOrganizationResponses]; + +export type AdminGetOrganizationData = { + body?: never; + path: { + /** + * Org Id + */ + org_id: string; + }; + query?: never; + url: '/api/v1/admin/organizations/{org_id}'; +}; + +export type AdminGetOrganizationErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminGetOrganizationError = AdminGetOrganizationErrors[keyof AdminGetOrganizationErrors]; + +export type AdminGetOrganizationResponses = { + /** + * Successful Response + */ + 200: OrganizationResponse; +}; + +export type AdminGetOrganizationResponse = AdminGetOrganizationResponses[keyof AdminGetOrganizationResponses]; + +export type AdminUpdateOrganizationData = { + body: OrganizationUpdate; + path: { + /** + * Org Id + */ + org_id: string; + }; + query?: never; + url: '/api/v1/admin/organizations/{org_id}'; +}; + +export type AdminUpdateOrganizationErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminUpdateOrganizationError = AdminUpdateOrganizationErrors[keyof AdminUpdateOrganizationErrors]; + +export type AdminUpdateOrganizationResponses = { + /** + * Successful Response + */ + 200: OrganizationResponse; +}; + +export type AdminUpdateOrganizationResponse = AdminUpdateOrganizationResponses[keyof AdminUpdateOrganizationResponses]; + +export type AdminListOrganizationMembersData = { + body?: never; + path: { + /** + * Org Id + */ + org_id: string; + }; + query?: { + /** + * Is Active + * + * Filter by active status + */ + is_active?: boolean | null; + /** + * Page + */ + page?: number; + /** + * Limit + */ + limit?: number; + }; + url: '/api/v1/admin/organizations/{org_id}/members'; +}; + +export type AdminListOrganizationMembersErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminListOrganizationMembersError = AdminListOrganizationMembersErrors[keyof AdminListOrganizationMembersErrors]; + +export type AdminListOrganizationMembersResponses = { + /** + * Successful Response + */ + 200: PaginatedResponseOrganizationMemberResponse; +}; + +export type AdminListOrganizationMembersResponse = AdminListOrganizationMembersResponses[keyof AdminListOrganizationMembersResponses]; + +export type AdminAddOrganizationMemberData = { + body: AddMemberRequest; + path: { + /** + * Org Id + */ + org_id: string; + }; + query?: never; + url: '/api/v1/admin/organizations/{org_id}/members'; +}; + +export type AdminAddOrganizationMemberErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminAddOrganizationMemberError = AdminAddOrganizationMemberErrors[keyof AdminAddOrganizationMemberErrors]; + +export type AdminAddOrganizationMemberResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type AdminAddOrganizationMemberResponse = AdminAddOrganizationMemberResponses[keyof AdminAddOrganizationMemberResponses]; + +export type AdminRemoveOrganizationMemberData = { + body?: never; + path: { + /** + * Org Id + */ + org_id: string; + /** + * User Id + */ + user_id: string; + }; + query?: never; + url: '/api/v1/admin/organizations/{org_id}/members/{user_id}'; +}; + +export type AdminRemoveOrganizationMemberErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AdminRemoveOrganizationMemberError = AdminRemoveOrganizationMemberErrors[keyof AdminRemoveOrganizationMemberErrors]; + +export type AdminRemoveOrganizationMemberResponses = { + /** + * Successful Response + */ + 200: MessageResponse; +}; + +export type AdminRemoveOrganizationMemberResponse = AdminRemoveOrganizationMemberResponses[keyof AdminRemoveOrganizationMemberResponses]; + +export type GetMyOrganizationsData = { + body?: never; + path?: never; + query?: { + /** + * Is Active + * + * Filter by active membership + */ + is_active?: boolean; + }; + url: '/api/v1/organizations/me'; +}; + +export type GetMyOrganizationsErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetMyOrganizationsError = GetMyOrganizationsErrors[keyof GetMyOrganizationsErrors]; + +export type GetMyOrganizationsResponses = { + /** + * Response Get My Organizations + * + * Successful Response + */ + 200: Array; +}; + +export type GetMyOrganizationsResponse = GetMyOrganizationsResponses[keyof GetMyOrganizationsResponses]; + +export type GetOrganizationData = { + body?: never; + path: { + /** + * Organization Id + */ + organization_id: string; + }; + query?: never; + url: '/api/v1/organizations/{organization_id}'; +}; + +export type GetOrganizationErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetOrganizationError = GetOrganizationErrors[keyof GetOrganizationErrors]; + +export type GetOrganizationResponses = { + /** + * Successful Response + */ + 200: OrganizationResponse; +}; + +export type GetOrganizationResponse = GetOrganizationResponses[keyof GetOrganizationResponses]; + +export type UpdateOrganizationData = { + body: OrganizationUpdate; + path: { + /** + * Organization Id + */ + organization_id: string; + }; + query?: never; + url: '/api/v1/organizations/{organization_id}'; +}; + +export type UpdateOrganizationErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateOrganizationError = UpdateOrganizationErrors[keyof UpdateOrganizationErrors]; + +export type UpdateOrganizationResponses = { + /** + * Successful Response + */ + 200: OrganizationResponse; +}; + +export type UpdateOrganizationResponse = UpdateOrganizationResponses[keyof UpdateOrganizationResponses]; + +export type GetOrganizationMembersData = { + body?: never; + path: { + /** + * Organization Id + */ + organization_id: string; + }; + query?: { + /** + * Is Active + * + * Filter by active status + */ + is_active?: boolean; + /** + * Page + */ + page?: number; + /** + * Limit + */ + limit?: number; + }; + url: '/api/v1/organizations/{organization_id}/members'; +}; + +export type GetOrganizationMembersErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetOrganizationMembersError = GetOrganizationMembersErrors[keyof GetOrganizationMembersErrors]; + +export type GetOrganizationMembersResponses = { + /** + * Successful Response + */ + 200: PaginatedResponseOrganizationMemberResponse; +}; + +export type GetOrganizationMembersResponse = GetOrganizationMembersResponses[keyof GetOrganizationMembersResponses]; diff --git a/frontend/tests/stores/authStore.test.ts b/frontend/tests/stores/authStore.test.ts index 51f38ae..11c6d79 100644 --- a/frontend/tests/stores/authStore.test.ts +++ b/frontend/tests/stores/authStore.test.ts @@ -21,6 +21,11 @@ describe('Auth Store', () => { }); jest.clearAllMocks(); + + // Reset storage mocks to default successful implementations + (storage.saveTokens as jest.Mock).mockResolvedValue(undefined); + (storage.getTokens as jest.Mock).mockResolvedValue(null); + (storage.clearTokens as jest.Mock).mockResolvedValue(undefined); }); describe('User validation', () => {