From 22c274bb83ef80e1388377611888033190d6935d Mon Sep 17 00:00:00 2001 From: echo Date: Tue, 10 Mar 2026 02:22:11 +0100 Subject: [PATCH] routes fix extensive loging added --- AGENTS.md | 366 ++++++++++ .../api/fitness-goals/[id]/complete/route.ts | 89 ++- .../src/app/api/fitness-goals/[id]/route.ts | 246 ++++--- apps/admin/src/app/api/fitness-goals/route.ts | 212 +++--- apps/admin/src/app/api/test/route.ts | 5 + apps/admin/src/lib/auth-helper.ts | 47 ++ apps/admin/src/middleware.ts | 45 +- apps/mobile/src/app/(tabs)/goals.tsx | 650 ++++++++++-------- apps/mobile/src/app/_layout.tsx | 100 ++- 9 files changed, 1252 insertions(+), 508 deletions(-) create mode 100644 AGENTS.md create mode 100644 apps/admin/src/app/api/test/route.ts create mode 100644 apps/admin/src/lib/auth-helper.ts diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6402712 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,366 @@ +# Agent Development Guide for FitAI + +This document provides essential guidelines for AI coding agents working in the FitAI codebase. + +## Project Overview + +FitAI is a monorepo containing a fitness AI platform with: + +- **Admin App** (`apps/admin`): Next.js 16 web application for gym management +- **Mobile App** (`apps/mobile`): React Native/Expo mobile app for clients +- **Shared Packages**: Database (`@fitai/database`) and utilities (`@fitai/shared`) + +**Tech Stack**: TypeScript, Next.js 16, React 19, React Native, Expo 54, Drizzle ORM, Clerk Auth, TanStack Query, Tailwind CSS + +## Build, Lint, and Test Commands + +### From Repository Root (`/home/echo/dev/prototype`) + +```bash +# Development +npm run dev # Start both admin and mobile dev servers +npm run dev:admin # Start admin dev server (Next.js) +npm run dev:mobile # Start mobile dev server (Expo) + +# Build +npm run build # Build both apps +npm run build:admin # Build admin app +npm run build:mobile # Build mobile app + +# Linting +npm run lint # Lint both apps +npm run lint:admin # Lint admin app +npm run lint:mobile # Lint mobile app + +# Type Checking +npm run typecheck # Check types in both apps +npm run typecheck:admin # Check types in admin +npm run typecheck:mobile # Check types in mobile + +# Testing +npm run test # Run all tests +npm run test:admin # Run admin tests +npm run test:mobile # Run mobile tests +``` + +### Admin App (`apps/admin`) + +```bash +cd apps/admin + +# Development +npm run dev # Start Next.js dev server (port 3000) + +# Testing +npm test # Run all tests +npx jest # Run all tests +npx jest path/to/file.test.ts # Run specific test file +npx jest --testPathPattern=drizzle # Run tests matching pattern +npx jest src/lib/database/__tests__/drizzle.test.ts # Run specific test + +# Build & Lint +npm run build # Build production bundle +npm run lint # Run ESLint +npm run typecheck # Type check without emitting +``` + +### Mobile App (`apps/mobile`) + +```bash +cd apps/mobile + +# Development +npm start # Start Expo dev server +npm run android # Start on Android emulator +npm run ios # Start on iOS simulator + +# Testing +npm test # Run all tests +npx jest path/to/file.test.ts # Run specific test file +npx jest --testPathPattern=component # Run tests matching pattern + +# Build & Lint +npm run build # Build with Expo +npm run lint # Run ESLint +npm run typecheck # Type check +``` + +## Code Style Guidelines + +### Import Organization + +**Order**: External libraries → Internal imports, grouped logically + +```typescript +// External: React, Next.js, React Native +import { NextRequest, NextResponse } from "next/server"; +import React from "react"; + +// External: Third-party libraries +import { auth, clerkClient } from "@clerk/nextjs/server"; +import bcrypt from "bcryptjs"; + +// Monorepo packages +import { db, sql } from "@fitai/database"; + +// Internal: Path aliases or relative imports +import { getDatabase } from "@/lib/database"; +import type { User } from "./types"; +``` + +**Key Rules**: + +- Use `import type { ... }` for type-only imports +- Admin app uses path aliases: `@/*` → `./src/*` +- Mobile app uses relative paths or `@/*` alias +- No automatic import sorting; manual grouping by category + +### Component Structure & Naming + +**Components**: PascalCase function components with named exports + +```typescript +// Feature components - Named export (function) +export function UserManagement() { + return
...
+} + +// UI library components - Named export (forwardRef constant) +const Card = React.forwardRef( + ({ className, ...props }, ref) => ( +
+ ) +) +Card.displayName = "Card" +export { Card } +``` + +**Naming Conventions**: + +- Components: `UserManagement`, `GoalProgressCard` +- Functions: `handleEditUser`, `getGreeting` +- Custom hooks: `useUser`, `useAuth` +- API routes: Named exports `GET`, `POST`, `PUT`, `DELETE` + +### Type Definitions + +**Interfaces** for object shapes and React props; **Types** for unions and aliases + +```typescript +// Props interfaces (ComponentNameProps pattern) +interface UserManagementProps { + userId: string; + onUpdate?: () => void; +} + +// Domain interfaces +interface User { + id: string; + email: string; + firstName: string; + lastName: string; +} + +// Type aliases and unions +type UserRole = "admin" | "trainer" | "client"; +type Status = "active" | "inactive" | "pending"; + +// No I prefix or T prefix +``` + +### Function Patterns + +**Arrow functions** for components and handlers; **function declarations** for utilities + +```typescript +// Component functions +export function UserCard() { + const handleClick = () => { /* ... */ } // Arrow function for handlers + return +} + +// Exported utility functions +export async function setUserRole(userId: string, role: UserRole) { + // ... implementation +} + +// Always use async/await (never .then() chains) +const fetchData = async () => { + try { + const response = await fetch(url) + const data = await response.json() + return data + } catch (error) { + console.error('Failed to fetch:', error) + } +} +``` + +### Error Handling + +**API Routes** (Next.js): + +```typescript +export async function POST(request: NextRequest) { + try { + const { email, password } = await request.json(); + + // Early returns for validation + if (!email || !password) { + return NextResponse.json( + { error: "Email and password are required" }, + { status: 400 }, + ); + } + + // ... logic + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Login error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} +``` + +**React Components**: + +```typescript +// Admin: Use try-catch with console.error +const handleSave = async () => { + try { + await updateUser(userId, data); + } catch (error) { + console.error("Failed to update user:", error); + // Show error to user (toast, alert, etc.) + } +}; + +// Mobile: Use Alert for confirmations +const handleDelete = () => { + Alert.alert("Delete Goal", "Are you sure?", [ + { text: "Cancel", style: "cancel" }, + { text: "Delete", style: "destructive", onPress: onDelete }, + ]); +}; +``` + +### Comments & Documentation + +Use JSDoc for public APIs; inline comments for complex logic only + +```typescript +/** + * Set a user's role in Clerk public metadata + * + * @param userId - Clerk user ID + * @param role - Role to assign (admin, trainer, or client) + * @returns Updated user object + * + * @example + * await setUserRole('user_abc123', 'admin') + */ +export async function setUserRole(userId: string, role: UserRole) { + // Implementation +} + +// Inline comments for clarification +// Optimistically update local state so grid reflects changes immediately +setUsers((prev) => prev.map((u) => (u.id === id ? { ...u, ...updates } : u))); +``` + +### File Naming Conventions + +``` +apps/admin/src/ +├── app/ +│ ├── api/*/route.ts # API routes (lowercase route.ts) +│ └── */page.tsx # Pages (lowercase page.tsx) +├── components/ +│ ├── ui/button.tsx # UI primitives (kebab-case) +│ └── users/UserManagement.tsx # Feature components (PascalCase) +├── lib/ +│ ├── database/index.ts # Utilities (kebab-case) +│ └── clerk-helpers.ts # Helpers (kebab-case) + +apps/mobile/src/ +├── app/ +│ └── (tabs)/index.tsx # Routes (lowercase) +├── components/ +│ └── GoalProgressCard.tsx # All components (PascalCase) +└── services/ + └── fitnessGoals.ts # Services (camelCase) +``` + +### Styling + +**Admin App**: Tailwind CSS with utility classes + +```typescript +
+ +
+``` + +**Mobile App**: StyleSheet.create() at file bottom with theme system + +```typescript +import { theme } from '../styles/theme' + +export function MyComponent() { + return ... +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + padding: theme.spacing.md, + }, +}) +``` + +## ESLint Rules (Admin) + +- `@typescript-eslint/no-unused-vars`: error +- `@typescript-eslint/no-explicit-any`: warn (allowed but discouraged) +- `prefer-const`: error +- `no-var`: error +- Extends: `next/core-web-vitals`, `@typescript-eslint/recommended` + +## TypeScript Configuration + +- **Strict mode enabled**: All strict type checking options on +- **Path aliases**: `@/*` maps to `./src/*` +- **Module resolution**: `bundler` (admin), `node` (mobile) +- **Target**: ES5 (admin), ESNext (mobile) +- Always provide explicit return types for exported functions + +## Testing Best Practices + +- Tests in `__tests__/` directories or `*.test.ts` files +- Use `@jest-environment node` comment for Node.js API tests +- Admin: Jest + ts-jest + @testing-library/react +- Mobile: Jest + react-native preset + @testing-library/react-native +- Test file naming: `component.test.ts` or `feature.test.tsx` +- Always test error cases and edge cases + +## Key Patterns + +1. **State Management**: Multiple `useState` declarations grouped together +2. **Destructuring**: Props in function signature, responses inline +3. **Type Safety**: Explicit return types, const assertions for readonly arrays +4. **Database**: Factory pattern with singleton (`getDatabase()`) +5. **Forms**: React Hook Form + Zod validation +6. **Data Fetching**: TanStack Query for server state +7. **Authentication**: Clerk for both admin and mobile (different packages) + +## Node & Package Manager + +- **Node**: >=18.0.0 +- **Package Manager**: npm (>=9.0.0) +- Use `npm install` (not yarn or pnpm) diff --git a/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts b/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts index 28a8504..87d4e68 100644 --- a/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts +++ b/apps/admin/src/app/api/fitness-goals/[id]/complete/route.ts @@ -1,39 +1,62 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { auth } from '@clerk/nextjs/server'; -import { getDatabase } from '@/lib/database'; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { getDatabase } from "@/lib/database"; + +// Helper to add CORS headers +function corsHeaders() { + return { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }; +} + +// OPTIONS - Handle preflight requests +export async function OPTIONS() { + return NextResponse.json({}, { headers: corsHeaders() }); +} // POST - Mark goal as complete export async function POST( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } + req: NextRequest, + { params }: { params: Promise<{ id: string }> }, ) { - try { - const { userId } = await auth(); - if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const { id } = await params; - const db = await getDatabase(); - - // Verify goal exists and user owns it - const existingGoal = await db.getFitnessGoalById(id); - if (!existingGoal) { - return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); - } - if (existingGoal.userId !== userId) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - // Mark as completed - const completedGoal = await db.completeGoal(id); - - return NextResponse.json(completedGoal); - } catch (error) { - console.error('Error completing fitness goal:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401, headers: corsHeaders() }, + ); } + + const { id } = await params; + const db = await getDatabase(); + + // Verify goal exists and user owns it + const existingGoal = await db.getFitnessGoalById(id); + if (!existingGoal) { + return NextResponse.json( + { error: "Goal not found" }, + { status: 404, headers: corsHeaders() }, + ); + } + if (existingGoal.userId !== userId) { + return NextResponse.json( + { error: "Forbidden" }, + { status: 403, headers: corsHeaders() }, + ); + } + + // Mark as completed + const completedGoal = await db.completeGoal(id); + + return NextResponse.json(completedGoal, { headers: corsHeaders() }); + } catch (error) { + console.error("Error completing fitness goal:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500, headers: corsHeaders() }, + ); + } } diff --git a/apps/admin/src/app/api/fitness-goals/[id]/route.ts b/apps/admin/src/app/api/fitness-goals/[id]/route.ts index d0f6fb5..701c2a7 100644 --- a/apps/admin/src/app/api/fitness-goals/[id]/route.ts +++ b/apps/admin/src/app/api/fitness-goals/[id]/route.ts @@ -1,119 +1,163 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { auth } from '@clerk/nextjs/server'; -import { getDatabase } from '@/lib/database'; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { getDatabase } from "@/lib/database"; + +// Helper to add CORS headers +function corsHeaders() { + return { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }; +} + +// OPTIONS - Handle preflight requests +export async function OPTIONS() { + return NextResponse.json({}, { headers: corsHeaders() }); +} // GET - Get specific goal export async function GET( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } + req: NextRequest, + { params }: { params: Promise<{ id: string }> }, ) { - try { - const { userId } = await auth(); - if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const { id } = await params; - const db = await getDatabase(); - - const goal = await db.getFitnessGoalById(id); - - if (!goal) { - return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); - } - - // Verify ownership - if (goal.userId !== userId) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - return NextResponse.json(goal); - } catch (error) { - console.error('Error fetching fitness goal:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401, headers: corsHeaders() }, + ); } + + const { id } = await params; + const db = await getDatabase(); + + const goal = await db.getFitnessGoalById(id); + + if (!goal) { + return NextResponse.json( + { error: "Goal not found" }, + { status: 404, headers: corsHeaders() }, + ); + } + + // Verify ownership + if (goal.userId !== userId) { + return NextResponse.json( + { error: "Forbidden" }, + { status: 403, headers: corsHeaders() }, + ); + } + + return NextResponse.json(goal, { headers: corsHeaders() }); + } catch (error) { + console.error("Error fetching fitness goal:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500, headers: corsHeaders() }, + ); + } } // PUT - Update goal export async function PUT( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } + req: NextRequest, + { params }: { params: Promise<{ id: string }> }, ) { - try { - const { userId } = await auth(); - if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const { id } = await params; - const db = await getDatabase(); - - // Verify goal exists and user owns it - const existingGoal = await db.getFitnessGoalById(id); - if (!existingGoal) { - return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); - } - if (existingGoal.userId !== userId) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const updates = await req.json(); - - // Don't allow changing userId or id - delete updates.userId; - delete updates.id; - delete updates.createdAt; - - const updatedGoal = await db.updateFitnessGoal(id, updates); - - return NextResponse.json(updatedGoal); - } catch (error) { - console.error('Error updating fitness goal:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401, headers: corsHeaders() }, + ); } + + const { id } = await params; + const db = await getDatabase(); + + // Verify goal exists and user owns it + const existingGoal = await db.getFitnessGoalById(id); + if (!existingGoal) { + return NextResponse.json( + { error: "Goal not found" }, + { status: 404, headers: corsHeaders() }, + ); + } + if (existingGoal.userId !== userId) { + return NextResponse.json( + { error: "Forbidden" }, + { status: 403, headers: corsHeaders() }, + ); + } + + const updates = await req.json(); + + // Don't allow changing userId or id + delete updates.userId; + delete updates.id; + delete updates.createdAt; + + const updatedGoal = await db.updateFitnessGoal(id, updates); + + return NextResponse.json(updatedGoal, { headers: corsHeaders() }); + } catch (error) { + console.error("Error updating fitness goal:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500, headers: corsHeaders() }, + ); + } } // DELETE - Delete goal export async function DELETE( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } + req: NextRequest, + { params }: { params: Promise<{ id: string }> }, ) { - try { - const { userId } = await auth(); - if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } - - const { id } = await params; - const db = await getDatabase(); - - // Verify goal exists and user owns it - const existingGoal = await db.getFitnessGoalById(id); - if (!existingGoal) { - return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); - } - if (existingGoal.userId !== userId) { - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); - } - - const deleted = await db.deleteFitnessGoal(id); - - if (deleted) { - return NextResponse.json({ success: true }); - } else { - return NextResponse.json({ error: 'Failed to delete goal' }, { status: 500 }); - } - } catch (error) { - console.error('Error deleting fitness goal:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + try { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401, headers: corsHeaders() }, + ); } + + const { id } = await params; + const db = await getDatabase(); + + // Verify goal exists and user owns it + const existingGoal = await db.getFitnessGoalById(id); + if (!existingGoal) { + return NextResponse.json( + { error: "Goal not found" }, + { status: 404, headers: corsHeaders() }, + ); + } + if (existingGoal.userId !== userId) { + return NextResponse.json( + { error: "Forbidden" }, + { status: 403, headers: corsHeaders() }, + ); + } + + const deleted = await db.deleteFitnessGoal(id); + + if (deleted) { + return NextResponse.json({ success: true }, { headers: corsHeaders() }); + } else { + return NextResponse.json( + { error: "Failed to delete goal" }, + { status: 500, headers: corsHeaders() }, + ); + } + } catch (error) { + console.error("Error deleting fitness goal:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500, headers: corsHeaders() }, + ); + } } diff --git a/apps/admin/src/app/api/fitness-goals/route.ts b/apps/admin/src/app/api/fitness-goals/route.ts index c82c3f3..ab82724 100644 --- a/apps/admin/src/app/api/fitness-goals/route.ts +++ b/apps/admin/src/app/api/fitness-goals/route.ts @@ -1,95 +1,141 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { auth } from '@clerk/nextjs/server'; -import { getDatabase } from '@/lib/database'; -import { randomBytes } from 'crypto'; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { getDatabase } from "@/lib/database"; +import { randomBytes } from "crypto"; + +// Helper to add CORS headers +function corsHeaders() { + return { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }; +} + +// Helper to get user ID from auth (works with both web sessions and mobile tokens) +async function getAuthenticatedUserId( + req: NextRequest, +): Promise { + // The auth() function from Clerk should handle Bearer tokens automatically + // when called within an API route that's processed by clerkMiddleware + const { userId } = await auth(); + return userId; +} + +// OPTIONS - Handle preflight requests +export async function OPTIONS() { + return NextResponse.json({}, { headers: corsHeaders() }); +} // GET - List user's fitness goals export async function GET(req: NextRequest) { - try { - const { userId } = await auth(); - if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } + try { + const userId = await getAuthenticatedUserId(req); - const { searchParams } = new URL(req.url); - const targetUserId = searchParams.get('userId') || userId; - const status = searchParams.get('status'); // active, completed, all - - const db = await getDatabase(); - - // Fetch goals with optional status filter - const goals = await db.getFitnessGoalsByUserId( - targetUserId, - status && status !== 'all' ? status : undefined - ); - - return NextResponse.json(goals); - } catch (error) { - console.error('Error fetching fitness goals:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + if (!userId) { + console.error("[Fitness Goals API] Authentication failed"); + console.error( + "[Fitness Goals API] Headers:", + Object.fromEntries(req.headers.entries()), + ); + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401, headers: corsHeaders() }, + ); } + + console.log("[Fitness Goals API] Authenticated user:", userId); + + const { searchParams } = new URL(req.url); + const targetUserId = searchParams.get("userId") || userId; + const status = searchParams.get("status"); // active, completed, all + + const db = await getDatabase(); + + // Fetch goals with optional status filter + const goals = await db.getFitnessGoalsByUserId( + targetUserId, + status && status !== "all" ? status : undefined, + ); + + console.log( + `[Fitness Goals API] Found ${goals.length} goals for user ${targetUserId}`, + ); + return NextResponse.json(goals, { headers: corsHeaders() }); + } catch (error) { + console.error("[Fitness Goals API] Error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500, headers: corsHeaders() }, + ); + } } // POST - Create new fitness goal export async function POST(req: NextRequest) { - try { - const { userId } = await auth(); - if (!userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - } + try { + const userId = await getAuthenticatedUserId(req); - const body = await req.json(); - const { - goalType, - title, - description, - targetValue, - currentValue, - unit, - targetDate, - priority, - notes, - fitnessProfileId - } = body; - - // Validation - if (!goalType || !title) { - return NextResponse.json( - { error: 'goalType and title are required' }, - { status: 400 } - ); - } - - const db = await getDatabase(); - - // Create the goal - const goal = await db.createFitnessGoal({ - id: randomBytes(16).toString('hex'), - userId, - fitnessProfileId: fitnessProfileId || undefined, - goalType, - title, - description: description || undefined, - targetValue: targetValue || undefined, - currentValue: currentValue || 0, - unit: unit || undefined, - startDate: new Date(), - targetDate: targetDate ? new Date(targetDate) : undefined, - status: 'active', - progress: 0, - priority: priority || 'medium', - notes: notes || undefined - }); - - return NextResponse.json(goal, { status: 201 }); - } catch (error) { - console.error('Error creating fitness goal:', error); - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ); + if (!userId) { + console.error("[Fitness Goals API] Authentication failed for POST"); + return NextResponse.json( + { error: "Unauthorized" }, + { status: 401, headers: corsHeaders() }, + ); } + + const body = await req.json(); + const { + goalType, + title, + description, + targetValue, + currentValue, + unit, + targetDate, + priority, + notes, + fitnessProfileId, + } = body; + + // Validation + if (!goalType || !title) { + return NextResponse.json( + { error: "goalType and title are required" }, + { status: 400, headers: corsHeaders() }, + ); + } + + const db = await getDatabase(); + + // Create the goal + const goal = await db.createFitnessGoal({ + id: randomBytes(16).toString("hex"), + userId, + fitnessProfileId: fitnessProfileId || undefined, + goalType, + title, + description: description || undefined, + targetValue: targetValue || undefined, + currentValue: currentValue || 0, + unit: unit || undefined, + startDate: new Date(), + targetDate: targetDate ? new Date(targetDate) : undefined, + status: "active", + progress: 0, + priority: priority || "medium", + notes: notes || undefined, + }); + + console.log( + `[Fitness Goals API] Created goal ${goal.id} for user ${userId}`, + ); + return NextResponse.json(goal, { status: 201, headers: corsHeaders() }); + } catch (error) { + console.error("[Fitness Goals API] Error creating goal:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500, headers: corsHeaders() }, + ); + } } diff --git a/apps/admin/src/app/api/test/route.ts b/apps/admin/src/app/api/test/route.ts new file mode 100644 index 0000000..1681868 --- /dev/null +++ b/apps/admin/src/app/api/test/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + return NextResponse.json({ message: "Test endpoint works!" }); +} diff --git a/apps/admin/src/lib/auth-helper.ts b/apps/admin/src/lib/auth-helper.ts new file mode 100644 index 0000000..2547096 --- /dev/null +++ b/apps/admin/src/lib/auth-helper.ts @@ -0,0 +1,47 @@ +import { auth, currentUser } from "@clerk/nextjs/server"; +import { NextRequest } from "next/server"; + +/** + * Get authenticated user ID from request + * Handles both session-based auth (web) and Bearer token auth (mobile) + * + * For mobile apps using Clerk Expo, tokens should be passed as: + * Authorization: Bearer + */ +export async function getAuthUserId(req: NextRequest): Promise { + try { + // Clerk's auth() should handle both cookies and Bearer tokens automatically + // when the request is properly formatted + const { userId } = await auth(); + + if (userId) { + console.log("✓ Authenticated user:", userId); + return userId; + } + + console.log("✗ No authentication found"); + + // Log headers for debugging + const authHeader = req.headers.get("authorization"); + console.log("Authorization header:", authHeader ? "Present" : "Missing"); + + return null; + } catch (error) { + console.error("Authentication error:", error); + return null; + } +} + +/** + * Simplified version that just uses Clerk's built-in auth + * This should work with both session cookies and Bearer tokens + */ +export async function requireAuth(req: NextRequest): Promise { + const userId = await getAuthUserId(req); + + if (!userId) { + throw new Error("Unauthorized"); + } + + return userId; +} diff --git a/apps/admin/src/middleware.ts b/apps/admin/src/middleware.ts index 0ae9460..83e7761 100644 --- a/apps/admin/src/middleware.ts +++ b/apps/admin/src/middleware.ts @@ -1,30 +1,43 @@ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; -// Define routes that should be publicly accessible +// Define routes that should be publicly accessible (no auth required) const isPublicRoute = createRouteMatcher([ "/sign-in(.*)", "/sign-up(.*)", "/api/webhooks(.*)", - "/api/attendance(.*)", + "/api/health(.*)", ]); -// Define routes that require authentication -const isProtectedRoute = createRouteMatcher([ - "/", - "/users(.*)", - "/analytics(.*)", - "/profile(.*)", - "/api/users(.*)", - "/api/profile(.*)", - "/api/payments(.*)", - "/api/notifications(.*)", -]); +// Define API routes that need auth but should be handled in the route itself +// This prevents auth.protect() from blocking before the route handler runs +const isApiRoute = createRouteMatcher(["/api/(.*)"]); export default clerkMiddleware(async (auth, req) => { - // Protect all routes except public ones - if (!isPublicRoute(req)) { - await auth.protect(); + // Log for debugging + const authHeader = req.headers.get("authorization"); + if (authHeader) { + console.log( + "[Middleware] Authorization header present:", + authHeader.substring(0, 20) + "...", + ); } + + // Don't protect public routes + if (isPublicRoute(req)) { + console.log("[Middleware] Public route, skipping auth"); + return; + } + + // For API routes, let the route handler check auth + // This allows API routes to handle both web sessions and mobile Bearer tokens + if (isApiRoute(req)) { + console.log("[Middleware] API route, auth will be checked in handler"); + return; + } + + // For all other routes (web pages), enforce authentication + console.log("[Middleware] Protected route, requiring auth"); + await auth.protect(); }); export const config = { diff --git a/apps/mobile/src/app/(tabs)/goals.tsx b/apps/mobile/src/app/(tabs)/goals.tsx index a264f63..b36f5e3 100644 --- a/apps/mobile/src/app/(tabs)/goals.tsx +++ b/apps/mobile/src/app/(tabs)/goals.tsx @@ -1,290 +1,392 @@ -import React, { useState, useCallback, useRef } from 'react'; -import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity, Animated, Alert } from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { LinearGradient } from 'expo-linear-gradient'; -import { theme } from '../../styles/theme'; -import { GoalProgressCard } from '../../components/GoalProgressCard'; -import { GoalCreationModal } from '../../components/GoalCreationModal'; +import React, { useState, useCallback, useRef } from "react"; +import { + View, + Text, + StyleSheet, + ScrollView, + RefreshControl, + TouchableOpacity, + Animated, + Alert, +} from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { LinearGradient } from "expo-linear-gradient"; +import { theme } from "../../styles/theme"; +import { GoalProgressCard } from "../../components/GoalProgressCard"; +import { GoalCreationModal } from "../../components/GoalCreationModal"; import { useUser, useAuth } from "@clerk/clerk-expo"; -import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from '../../services/fitnessGoals'; -import { useFocusEffect } from 'expo-router'; +import { + fitnessGoalsService, + type FitnessGoal, + type CreateGoalData, +} from "../../services/fitnessGoals"; +import { useFocusEffect } from "expo-router"; +import * as SecureStore from "expo-secure-store"; export default function GoalsScreen() { - const { user } = useUser(); - const { getToken } = useAuth(); - const [goals, setGoals] = useState([]); - const [refreshing, setRefreshing] = useState(false); - const [isModalVisible, setIsModalVisible] = useState(false); - const fabScale = useRef(new Animated.Value(1)).current; + const { user } = useUser(); + const { getToken } = useAuth(); + const [goals, setGoals] = useState([]); + const [refreshing, setRefreshing] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const fabScale = useRef(new Animated.Value(1)).current; - const loadGoals = useCallback(async () => { - if (!user?.id) return; + const loadGoals = useCallback(async () => { + if (!user?.id) return; + try { + const token = await getToken(); + console.log( + "Token obtained:", + token ? "Yes (" + token.substring(0, 20) + "...)" : "No", + ); + console.log("User ID:", user.id); + + // Decode and log token details for debugging + if (token) { try { - const token = await getToken(); - const loadedGoals = await fitnessGoalsService.getGoals(user.id, token); - setGoals(loadedGoals); - } catch (error) { - console.error('Error loading goals:', error); + const parts = token.split("."); + if (parts.length === 3) { + const payload = JSON.parse(atob(parts[1])); + console.log("Token issuer:", payload.iss); + console.log( + "Token kid from header:", + JSON.parse(atob(parts[0])).kid, + ); + } + } catch (e) { + console.log("Could not decode token"); } - }, [user?.id]); // Removed getToken from dependencies + } - useFocusEffect( - useCallback(() => { - loadGoals(); - }, [loadGoals]) - ); + const loadedGoals = await fitnessGoalsService.getGoals(user.id, token); + setGoals(loadedGoals); + } catch (error) { + console.error("Error loading goals:", error); + } + }, [user?.id, getToken]); - const onRefresh = async () => { - setRefreshing(true); - await loadGoals(); - setRefreshing(false); - }; + const clearClerkCache = async () => { + Alert.alert( + "Clear Clerk Cache", + "This will clear all cached Clerk tokens. You will need to sign out and sign back in.", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Clear Cache", + style: "destructive", + onPress: async () => { + try { + // Clear all possible Clerk token keys + const keysToDelete = [ + "__clerk_client_jwt", + "__clerk_db_jwt", + "__clerk_client_uat", + "__clerk_session_id", + "__clerk_refresh_token", + "__clerk_session_jwt", + ]; - const handleCreateGoal = async (newGoal: CreateGoalData) => { - const token = await getToken(); - await fitnessGoalsService.createGoal(newGoal, token); - await loadGoals(); - setIsModalVisible(false); - }; - - const handleCompleteGoal = async (goal: FitnessGoal) => { - const token = await getToken(); - await fitnessGoalsService.completeGoal(goal.id, token); - await loadGoals(); - }; - - const handleDeleteGoal = async (goalId: string) => { - const token = await getToken(); - await fitnessGoalsService.deleteGoal(goalId, token); - await loadGoals(); - }; - - const activeGoals = goals.filter(g => g.status === 'active'); - const completedGoals = goals.filter(g => g.status === 'completed'); - - return ( - - + for (const key of keysToDelete) { + try { + await SecureStore.deleteItemAsync(key); + } catch (e) { + // Key might not exist } - > - - - Fitness Goals - - Track your fitness journey progress - - - + } - {/* Stats Summary */} - {goals.length > 0 && ( - - - {activeGoals.length} - Active - - - {completedGoals.length} - Completed - - - - {activeGoals.length > 0 - ? Math.round( - activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) / - activeGoals.length - ) - : 0}% - - Avg Progress - - - )} - - {/* Active Goals */} - - - Active Goals ({activeGoals.length}) - - {activeGoals.length === 0 ? ( - - - No active goals yet - - Tap the + button to create your first goal - - - ) : ( - activeGoals.map((goal) => ( - handleCompleteGoal(goal)} - onDelete={() => handleDeleteGoal(goal.id)} - /> - )) - )} - - - {/* Completed Goals */} - {completedGoals.length > 0 && ( - - - Completed Goals ({completedGoals.length}) - - {completedGoals.map((goal) => ( - handleDeleteGoal(goal.id)} - /> - ))} - - )} - - - - - {/* Floating Action Button */} - - setIsModalVisible(true)} - onPressIn={() => { - Animated.spring(fabScale, { - toValue: 0.9, - friction: 8, - tension: 100, - useNativeDriver: true, - }).start(); - }} - onPressOut={() => { - Animated.spring(fabScale, { - toValue: 1, - friction: 8, - tension: 100, - useNativeDriver: true, - }).start(); - }} - activeOpacity={0.9} - > - - - - - - - {/* Create Goal Modal */} - setIsModalVisible(false)} - onSubmit={handleCreateGoal} - /> - + Alert.alert( + "Success", + "Cache cleared! Please sign out and sign back in.", + ); + } catch (error) { + console.error("Error clearing cache:", error); + Alert.alert("Error", "Failed to clear cache"); + } + }, + }, + ], ); + }; + + useFocusEffect( + useCallback(() => { + loadGoals(); + }, [loadGoals]), + ); + + const onRefresh = async () => { + setRefreshing(true); + await loadGoals(); + setRefreshing(false); + }; + + const handleCreateGoal = async (newGoal: CreateGoalData) => { + const token = await getToken(); + await fitnessGoalsService.createGoal(newGoal, token); + await loadGoals(); + setIsModalVisible(false); + }; + + const handleCompleteGoal = async (goal: FitnessGoal) => { + const token = await getToken(); + await fitnessGoalsService.completeGoal(goal.id, token); + await loadGoals(); + }; + + const handleDeleteGoal = async (goalId: string) => { + const token = await getToken(); + await fitnessGoalsService.deleteGoal(goalId, token); + await loadGoals(); + }; + + const activeGoals = goals.filter((g) => g.status === "active"); + const completedGoals = goals.filter((g) => g.status === "completed"); + + return ( + + + } + > + + + + Fitness Goals + + Track your fitness journey progress + + + + + + + + + {/* Stats Summary */} + {goals.length > 0 && ( + + + {activeGoals.length} + Active + + + {completedGoals.length} + Completed + + + + {activeGoals.length > 0 + ? Math.round( + activeGoals.reduce( + (sum, g) => sum + (g.progress || 0), + 0, + ) / activeGoals.length, + ) + : 0} + % + + Avg Progress + + + )} + + {/* Active Goals */} + + + Active Goals ({activeGoals.length}) + + {activeGoals.length === 0 ? ( + + + No active goals yet + + Tap the + button to create your first goal + + + ) : ( + activeGoals.map((goal) => ( + handleCompleteGoal(goal)} + onDelete={() => handleDeleteGoal(goal.id)} + /> + )) + )} + + + {/* Completed Goals */} + {completedGoals.length > 0 && ( + + + Completed Goals ({completedGoals.length}) + + {completedGoals.map((goal) => ( + handleDeleteGoal(goal.id)} + /> + ))} + + )} + + + + + {/* Floating Action Button */} + + setIsModalVisible(true)} + onPressIn={() => { + Animated.spring(fabScale, { + toValue: 0.9, + friction: 8, + tension: 100, + useNativeDriver: true, + }).start(); + }} + onPressOut={() => { + Animated.spring(fabScale, { + toValue: 1, + friction: 8, + tension: 100, + useNativeDriver: true, + }).start(); + }} + activeOpacity={0.9} + > + + + + + + + {/* Create Goal Modal */} + setIsModalVisible(false)} + onSubmit={handleCreateGoal} + /> + + ); } const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: theme.colors.background, - }, - scrollContent: { - paddingBottom: 20, - }, - header: { - padding: 24, - paddingTop: 60, - paddingBottom: 24, - marginBottom: 10, - borderBottomLeftRadius: theme.borderRadius.xl, - borderBottomRightRadius: theme.borderRadius.xl, - }, - headerTitle: { - fontSize: theme.typography.fontSize['3xl'], - fontWeight: theme.typography.fontWeight.bold, - color: theme.colors.white, - }, - headerSubtitle: { - fontSize: theme.typography.fontSize.base, - color: "rgba(255, 255, 255, 0.9)", - marginTop: 4, - }, - statsContainer: { - flexDirection: "row", - padding: 16, - gap: 12, - }, - statCard: { - flex: 1, - backgroundColor: theme.colors.white, - padding: 16, - borderRadius: theme.borderRadius.xl, - alignItems: 'center', - ...theme.shadows.medium, - borderWidth: 1, - borderColor: "rgba(59, 130, 246, 0.1)", - }, - statValue: { - fontSize: theme.typography.fontSize['2xl'], - fontWeight: theme.typography.fontWeight.bold, - color: theme.colors.primary, - marginBottom: 4, - }, - statLabel: { - fontSize: 12, - color: "#6b7280", - fontWeight: "500", - }, - section: { - padding: 20, - paddingTop: 10, - }, - sectionTitle: { - fontSize: 18, - fontWeight: "600", - color: "#374151", - marginBottom: 12, - }, - emptyState: { - alignItems: "center", - paddingVertical: 40, - }, - emptyText: { - fontSize: 16, - fontWeight: "500", - color: "#6b7280", - marginTop: 12, - }, - emptySubtext: { - fontSize: 14, - color: "#9ca3af", - marginTop: 4, - }, - footer: { - height: 100, - }, - fabContainer: { - position: "absolute", - right: 20, - bottom: 110, // Adjusted for tab bar height - }, - fab: { - width: 64, - height: 64, - borderRadius: 32, - justifyContent: "center", - alignItems: "center", - ...theme.shadows.glow, - }, + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + scrollContent: { + paddingBottom: 20, + }, + header: { + padding: 24, + paddingTop: 60, + paddingBottom: 24, + marginBottom: 10, + borderBottomLeftRadius: theme.borderRadius.xl, + borderBottomRightRadius: theme.borderRadius.xl, + }, + headerContent: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + debugButton: { + padding: 8, + }, + headerTitle: { + fontSize: theme.typography.fontSize["3xl"], + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.white, + }, + headerSubtitle: { + fontSize: theme.typography.fontSize.base, + color: "rgba(255, 255, 255, 0.9)", + marginTop: 4, + }, + statsContainer: { + flexDirection: "row", + padding: 16, + gap: 12, + }, + statCard: { + flex: 1, + backgroundColor: theme.colors.white, + padding: 16, + borderRadius: theme.borderRadius.xl, + alignItems: "center", + ...theme.shadows.medium, + borderWidth: 1, + borderColor: "rgba(59, 130, 246, 0.1)", + }, + statValue: { + fontSize: theme.typography.fontSize["2xl"], + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.primary, + marginBottom: 4, + }, + statLabel: { + fontSize: 12, + color: "#6b7280", + fontWeight: "500", + }, + section: { + padding: 20, + paddingTop: 10, + }, + sectionTitle: { + fontSize: 18, + fontWeight: "600", + color: "#374151", + marginBottom: 12, + }, + emptyState: { + alignItems: "center", + paddingVertical: 40, + }, + emptyText: { + fontSize: 16, + fontWeight: "500", + color: "#6b7280", + marginTop: 12, + }, + emptySubtext: { + fontSize: 14, + color: "#9ca3af", + marginTop: 4, + }, + footer: { + height: 100, + }, + fabContainer: { + position: "absolute", + right: 20, + bottom: 110, // Adjusted for tab bar height + }, + fab: { + width: 64, + height: 64, + borderRadius: 32, + justifyContent: "center", + alignItems: "center", + ...theme.shadows.glow, + }, }); diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 69181f4..65b4023 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -2,12 +2,26 @@ import { ClerkProvider, ClerkLoaded } from "@clerk/clerk-expo"; import { Stack } from "expo-router"; import * as SecureStore from "expo-secure-store"; import { View, Text } from "react-native"; +import { useEffect, useState } from "react"; + +console.log("========================================"); +console.log("🚀 _layout.tsx loaded at:", new Date().toISOString()); +console.log( + "📦 EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY:", + process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY, +); +console.log("========================================"); // Token cache for Clerk const tokenCache = { async getToken(key: string) { try { - return SecureStore.getItemAsync(key); + const value = await SecureStore.getItemAsync(key); + console.log(`[TokenCache] Getting key: ${key}, exists: ${!!value}`); + if (value && value.length > 50) { + console.log(`[TokenCache] Value preview: ${value.substring(0, 50)}...`); + } + return value; } catch (err) { console.error("Error getting token:", err); return null; @@ -15,6 +29,10 @@ const tokenCache = { }, async saveToken(key: string, value: string) { try { + console.log(`[TokenCache] Saving key: ${key}`); + if (value && value.length > 50) { + console.log(`[TokenCache] Value preview: ${value.substring(0, 50)}...`); + } return SecureStore.setItemAsync(key, value); } catch (err) { console.error("Error saving token:", err); @@ -24,8 +42,71 @@ const tokenCache = { export default function RootLayout() { const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY; + const [cacheCleared, setCacheCleared] = useState(false); + + console.log( + "🔑 [RootLayout] Component rendering with publishableKey:", + publishableKey, + ); + + // TEMPORARY: Clear Clerk cache on app start to fix instance mismatch + useEffect(() => { + console.log("⚡ [RootLayout] useEffect triggered - starting cache cleanup"); + + const clearOldClerkCache = async () => { + try { + console.log("🔍 [RootLayout] Checking for old Clerk tokens..."); + + const keysToCheck = [ + "__clerk_client_jwt", + "__clerk_db_jwt", + "__clerk_client_uat", + "__clerk_session_id", + "__clerk_refresh_token", + "__clerk_session_jwt", + ]; + + for (const key of keysToCheck) { + try { + const value = await SecureStore.getItemAsync(key); + if (value) { + console.log(`📌 [RootLayout] Found token at ${key}`); + console.log( + `📝 [RootLayout] Token preview: ${value.substring(0, 80)}...`, + ); + + // Check if it's from the old instance + if (value.includes("pleasing-pheasant-20")) { + console.log(`🗑️ [RootLayout] DELETING old token from ${key}`); + await SecureStore.deleteItemAsync(key); + } else if (value.includes("needed-elephant-64")) { + console.log( + `✅ [RootLayout] Token is from correct instance (needed-elephant-64)`, + ); + } else { + console.log( + `⚠️ [RootLayout] Token doesn't match known instances`, + ); + } + } + } catch (e) { + console.log(`❌ [RootLayout] Error checking key ${key}:`, e); + } + } + + console.log("✅ [RootLayout] Old token cleanup complete"); + setCacheCleared(true); + } catch (error) { + console.error("❌ [RootLayout] Error clearing old cache:", error); + setCacheCleared(true); // Continue anyway + } + }; + + clearOldClerkCache(); + }, []); if (!publishableKey) { + console.log("❌ [RootLayout] No publishable key found!"); return ( Missing Clerk Publishable Key @@ -38,6 +119,23 @@ export default function RootLayout() { ); } + if (!cacheCleared) { + console.log("⏳ [RootLayout] Waiting for cache to clear..."); + return ( + + Clearing old authentication cache... + + Check the terminal for logs... + + + ); + } + + console.log( + "🚀 [RootLayout] Rendering ClerkProvider with key:", + publishableKey?.substring(0, 20) + "...", + ); + return (