routes fix

extensive loging added
This commit is contained in:
echo 2026-03-10 02:22:11 +01:00
parent 36315ea5cf
commit 22c274bb83
9 changed files with 1252 additions and 508 deletions

366
AGENTS.md Normal file
View File

@ -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 <div>...</div>
}
// UI library components - Named export (forwardRef constant)
const Card = React.forwardRef<HTMLDivElement, CardProps>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg", className)} {...props} />
)
)
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 <button onClick={handleClick}>Click</button>
}
// 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
<div className="flex items-center gap-2 rounded-lg border p-4">
<Button className="bg-primary text-white hover:bg-primary/90">
Click me
</Button>
</div>
```
**Mobile App**: StyleSheet.create() at file bottom with theme system
```typescript
import { theme } from '../styles/theme'
export function MyComponent() {
return <View style={styles.container}>...</View>
}
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)

View File

@ -1,16 +1,33 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from "next/server";
import { auth } from '@clerk/nextjs/server'; import { auth } from "@clerk/nextjs/server";
import { getDatabase } from '@/lib/database'; 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 // POST - Mark goal as complete
export async function POST( export async function POST(
req: NextRequest, req: NextRequest,
{ params }: { params: Promise<{ id: string }> } { params }: { params: Promise<{ id: string }> },
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
if (!userId) { if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401, headers: corsHeaders() },
);
} }
const { id } = await params; const { id } = await params;
@ -19,21 +36,27 @@ export async function POST(
// Verify goal exists and user owns it // Verify goal exists and user owns it
const existingGoal = await db.getFitnessGoalById(id); const existingGoal = await db.getFitnessGoalById(id);
if (!existingGoal) { if (!existingGoal) {
return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); return NextResponse.json(
{ error: "Goal not found" },
{ status: 404, headers: corsHeaders() },
);
} }
if (existingGoal.userId !== userId) { if (existingGoal.userId !== userId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); return NextResponse.json(
{ error: "Forbidden" },
{ status: 403, headers: corsHeaders() },
);
} }
// Mark as completed // Mark as completed
const completedGoal = await db.completeGoal(id); const completedGoal = await db.completeGoal(id);
return NextResponse.json(completedGoal); return NextResponse.json(completedGoal, { headers: corsHeaders() });
} catch (error) { } catch (error) {
console.error('Error completing fitness goal:', error); console.error("Error completing fitness goal:", error);
return NextResponse.json( return NextResponse.json(
{ error: 'Internal server error' }, { error: "Internal server error" },
{ status: 500 } { status: 500, headers: corsHeaders() },
); );
} }
} }

View File

@ -1,16 +1,33 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from "next/server";
import { auth } from '@clerk/nextjs/server'; import { auth } from "@clerk/nextjs/server";
import { getDatabase } from '@/lib/database'; 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 // GET - Get specific goal
export async function GET( export async function GET(
req: NextRequest, req: NextRequest,
{ params }: { params: Promise<{ id: string }> } { params }: { params: Promise<{ id: string }> },
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
if (!userId) { if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401, headers: corsHeaders() },
);
} }
const { id } = await params; const { id } = await params;
@ -19,20 +36,26 @@ export async function GET(
const goal = await db.getFitnessGoalById(id); const goal = await db.getFitnessGoalById(id);
if (!goal) { if (!goal) {
return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); return NextResponse.json(
{ error: "Goal not found" },
{ status: 404, headers: corsHeaders() },
);
} }
// Verify ownership // Verify ownership
if (goal.userId !== userId) { if (goal.userId !== userId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); return NextResponse.json(
{ error: "Forbidden" },
{ status: 403, headers: corsHeaders() },
);
} }
return NextResponse.json(goal); return NextResponse.json(goal, { headers: corsHeaders() });
} catch (error) { } catch (error) {
console.error('Error fetching fitness goal:', error); console.error("Error fetching fitness goal:", error);
return NextResponse.json( return NextResponse.json(
{ error: 'Internal server error' }, { error: "Internal server error" },
{ status: 500 } { status: 500, headers: corsHeaders() },
); );
} }
} }
@ -40,12 +63,15 @@ export async function GET(
// PUT - Update goal // PUT - Update goal
export async function PUT( export async function PUT(
req: NextRequest, req: NextRequest,
{ params }: { params: Promise<{ id: string }> } { params }: { params: Promise<{ id: string }> },
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
if (!userId) { if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401, headers: corsHeaders() },
);
} }
const { id } = await params; const { id } = await params;
@ -54,10 +80,16 @@ export async function PUT(
// Verify goal exists and user owns it // Verify goal exists and user owns it
const existingGoal = await db.getFitnessGoalById(id); const existingGoal = await db.getFitnessGoalById(id);
if (!existingGoal) { if (!existingGoal) {
return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); return NextResponse.json(
{ error: "Goal not found" },
{ status: 404, headers: corsHeaders() },
);
} }
if (existingGoal.userId !== userId) { if (existingGoal.userId !== userId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); return NextResponse.json(
{ error: "Forbidden" },
{ status: 403, headers: corsHeaders() },
);
} }
const updates = await req.json(); const updates = await req.json();
@ -69,12 +101,12 @@ export async function PUT(
const updatedGoal = await db.updateFitnessGoal(id, updates); const updatedGoal = await db.updateFitnessGoal(id, updates);
return NextResponse.json(updatedGoal); return NextResponse.json(updatedGoal, { headers: corsHeaders() });
} catch (error) { } catch (error) {
console.error('Error updating fitness goal:', error); console.error("Error updating fitness goal:", error);
return NextResponse.json( return NextResponse.json(
{ error: 'Internal server error' }, { error: "Internal server error" },
{ status: 500 } { status: 500, headers: corsHeaders() },
); );
} }
} }
@ -82,12 +114,15 @@ export async function PUT(
// DELETE - Delete goal // DELETE - Delete goal
export async function DELETE( export async function DELETE(
req: NextRequest, req: NextRequest,
{ params }: { params: Promise<{ id: string }> } { params }: { params: Promise<{ id: string }> },
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
if (!userId) { if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401, headers: corsHeaders() },
);
} }
const { id } = await params; const { id } = await params;
@ -96,24 +131,33 @@ export async function DELETE(
// Verify goal exists and user owns it // Verify goal exists and user owns it
const existingGoal = await db.getFitnessGoalById(id); const existingGoal = await db.getFitnessGoalById(id);
if (!existingGoal) { if (!existingGoal) {
return NextResponse.json({ error: 'Goal not found' }, { status: 404 }); return NextResponse.json(
{ error: "Goal not found" },
{ status: 404, headers: corsHeaders() },
);
} }
if (existingGoal.userId !== userId) { if (existingGoal.userId !== userId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); return NextResponse.json(
{ error: "Forbidden" },
{ status: 403, headers: corsHeaders() },
);
} }
const deleted = await db.deleteFitnessGoal(id); const deleted = await db.deleteFitnessGoal(id);
if (deleted) { if (deleted) {
return NextResponse.json({ success: true }); return NextResponse.json({ success: true }, { headers: corsHeaders() });
} else { } else {
return NextResponse.json({ error: 'Failed to delete goal' }, { status: 500 }); return NextResponse.json(
{ error: "Failed to delete goal" },
{ status: 500, headers: corsHeaders() },
);
} }
} catch (error) { } catch (error) {
console.error('Error deleting fitness goal:', error); console.error("Error deleting fitness goal:", error);
return NextResponse.json( return NextResponse.json(
{ error: 'Internal server error' }, { error: "Internal server error" },
{ status: 500 } { status: 500, headers: corsHeaders() },
); );
} }
} }

View File

@ -1,34 +1,72 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from "next/server";
import { auth } from '@clerk/nextjs/server'; import { auth } from "@clerk/nextjs/server";
import { getDatabase } from '@/lib/database'; import { getDatabase } from "@/lib/database";
import { randomBytes } from 'crypto'; 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<string | null> {
// 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 // GET - List user's fitness goals
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
try { try {
const { userId } = await auth(); const userId = await getAuthenticatedUserId(req);
if (!userId) { if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 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 { searchParams } = new URL(req.url);
const targetUserId = searchParams.get('userId') || userId; const targetUserId = searchParams.get("userId") || userId;
const status = searchParams.get('status'); // active, completed, all const status = searchParams.get("status"); // active, completed, all
const db = await getDatabase(); const db = await getDatabase();
// Fetch goals with optional status filter // Fetch goals with optional status filter
const goals = await db.getFitnessGoalsByUserId( const goals = await db.getFitnessGoalsByUserId(
targetUserId, targetUserId,
status && status !== 'all' ? status : undefined status && status !== "all" ? status : undefined,
); );
return NextResponse.json(goals); console.log(
`[Fitness Goals API] Found ${goals.length} goals for user ${targetUserId}`,
);
return NextResponse.json(goals, { headers: corsHeaders() });
} catch (error) { } catch (error) {
console.error('Error fetching fitness goals:', error); console.error("[Fitness Goals API] Error:", error);
return NextResponse.json( return NextResponse.json(
{ error: 'Internal server error' }, { error: "Internal server error" },
{ status: 500 } { status: 500, headers: corsHeaders() },
); );
} }
} }
@ -36,9 +74,14 @@ export async function GET(req: NextRequest) {
// POST - Create new fitness goal // POST - Create new fitness goal
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const { userId } = await auth(); const userId = await getAuthenticatedUserId(req);
if (!userId) { if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); console.error("[Fitness Goals API] Authentication failed for POST");
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401, headers: corsHeaders() },
);
} }
const body = await req.json(); const body = await req.json();
@ -52,14 +95,14 @@ export async function POST(req: NextRequest) {
targetDate, targetDate,
priority, priority,
notes, notes,
fitnessProfileId fitnessProfileId,
} = body; } = body;
// Validation // Validation
if (!goalType || !title) { if (!goalType || !title) {
return NextResponse.json( return NextResponse.json(
{ error: 'goalType and title are required' }, { error: "goalType and title are required" },
{ status: 400 } { status: 400, headers: corsHeaders() },
); );
} }
@ -67,7 +110,7 @@ export async function POST(req: NextRequest) {
// Create the goal // Create the goal
const goal = await db.createFitnessGoal({ const goal = await db.createFitnessGoal({
id: randomBytes(16).toString('hex'), id: randomBytes(16).toString("hex"),
userId, userId,
fitnessProfileId: fitnessProfileId || undefined, fitnessProfileId: fitnessProfileId || undefined,
goalType, goalType,
@ -78,18 +121,21 @@ export async function POST(req: NextRequest) {
unit: unit || undefined, unit: unit || undefined,
startDate: new Date(), startDate: new Date(),
targetDate: targetDate ? new Date(targetDate) : undefined, targetDate: targetDate ? new Date(targetDate) : undefined,
status: 'active', status: "active",
progress: 0, progress: 0,
priority: priority || 'medium', priority: priority || "medium",
notes: notes || undefined notes: notes || undefined,
}); });
return NextResponse.json(goal, { status: 201 }); console.log(
`[Fitness Goals API] Created goal ${goal.id} for user ${userId}`,
);
return NextResponse.json(goal, { status: 201, headers: corsHeaders() });
} catch (error) { } catch (error) {
console.error('Error creating fitness goal:', error); console.error("[Fitness Goals API] Error creating goal:", error);
return NextResponse.json( return NextResponse.json(
{ error: 'Internal server error' }, { error: "Internal server error" },
{ status: 500 } { status: 500, headers: corsHeaders() },
); );
} }
} }

View File

@ -0,0 +1,5 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Test endpoint works!" });
}

View File

@ -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 <token>
*/
export async function getAuthUserId(req: NextRequest): Promise<string | null> {
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<string> {
const userId = await getAuthUserId(req);
if (!userId) {
throw new Error("Unauthorized");
}
return userId;
}

View File

@ -1,30 +1,43 @@
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; 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([ const isPublicRoute = createRouteMatcher([
"/sign-in(.*)", "/sign-in(.*)",
"/sign-up(.*)", "/sign-up(.*)",
"/api/webhooks(.*)", "/api/webhooks(.*)",
"/api/attendance(.*)", "/api/health(.*)",
]); ]);
// Define routes that require authentication // Define API routes that need auth but should be handled in the route itself
const isProtectedRoute = createRouteMatcher([ // This prevents auth.protect() from blocking before the route handler runs
"/", const isApiRoute = createRouteMatcher(["/api/(.*)"]);
"/users(.*)",
"/analytics(.*)",
"/profile(.*)",
"/api/users(.*)",
"/api/profile(.*)",
"/api/payments(.*)",
"/api/notifications(.*)",
]);
export default clerkMiddleware(async (auth, req) => { export default clerkMiddleware(async (auth, req) => {
// Protect all routes except public ones // Log for debugging
if (!isPublicRoute(req)) { const authHeader = req.headers.get("authorization");
await auth.protect(); 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 = { export const config = {

View File

@ -1,13 +1,27 @@
import React, { useState, useCallback, useRef } from 'react'; import React, { useState, useCallback, useRef } from "react";
import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity, Animated, Alert } from 'react-native'; import {
import { Ionicons } from '@expo/vector-icons'; View,
import { LinearGradient } from 'expo-linear-gradient'; Text,
import { theme } from '../../styles/theme'; StyleSheet,
import { GoalProgressCard } from '../../components/GoalProgressCard'; ScrollView,
import { GoalCreationModal } from '../../components/GoalCreationModal'; 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 { useUser, useAuth } from "@clerk/clerk-expo";
import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from '../../services/fitnessGoals'; import {
import { useFocusEffect } from 'expo-router'; fitnessGoalsService,
type FitnessGoal,
type CreateGoalData,
} from "../../services/fitnessGoals";
import { useFocusEffect } from "expo-router";
import * as SecureStore from "expo-secure-store";
export default function GoalsScreen() { export default function GoalsScreen() {
const { user } = useUser(); const { user } = useUser();
@ -21,17 +35,83 @@ export default function GoalsScreen() {
if (!user?.id) return; if (!user?.id) return;
try { try {
const token = await getToken(); 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 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");
}
}
const loadedGoals = await fitnessGoalsService.getGoals(user.id, token); const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
setGoals(loadedGoals); setGoals(loadedGoals);
} catch (error) { } catch (error) {
console.error('Error loading goals:', error); console.error("Error loading goals:", error);
} }
}, [user?.id]); // Removed getToken from dependencies }, [user?.id, getToken]);
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",
];
for (const key of keysToDelete) {
try {
await SecureStore.deleteItemAsync(key);
} catch (e) {
// Key might not exist
}
}
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( useFocusEffect(
useCallback(() => { useCallback(() => {
loadGoals(); loadGoals();
}, [loadGoals]) }, [loadGoals]),
); );
const onRefresh = async () => { const onRefresh = async () => {
@ -59,27 +139,36 @@ export default function GoalsScreen() {
await loadGoals(); await loadGoals();
}; };
const activeGoals = goals.filter(g => g.status === 'active'); const activeGoals = goals.filter((g) => g.status === "active");
const completedGoals = goals.filter(g => g.status === 'completed'); const completedGoals = goals.filter((g) => g.status === "completed");
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ScrollView <ScrollView
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
refreshControl={ refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} /> <RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={theme.colors.primary}
/>
} }
> >
<LinearGradient <LinearGradient colors={theme.gradients.primary} style={styles.header}>
colors={theme.gradients.primary} <View style={styles.headerContent}>
style={styles.header}
>
<View> <View>
<Text style={styles.headerTitle}>Fitness Goals</Text> <Text style={styles.headerTitle}>Fitness Goals</Text>
<Text style={styles.headerSubtitle}> <Text style={styles.headerSubtitle}>
Track your fitness journey progress Track your fitness journey progress
</Text> </Text>
</View> </View>
<TouchableOpacity
onPress={clearClerkCache}
style={styles.debugButton}
>
<Ionicons name="refresh-circle-outline" size={24} color="#fff" />
</TouchableOpacity>
</View>
</LinearGradient> </LinearGradient>
{/* Stats Summary */} {/* Stats Summary */}
@ -97,10 +186,13 @@ export default function GoalsScreen() {
<Text style={styles.statValue}> <Text style={styles.statValue}>
{activeGoals.length > 0 {activeGoals.length > 0
? Math.round( ? Math.round(
activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) / activeGoals.reduce(
activeGoals.length (sum, g) => sum + (g.progress || 0),
0,
) / activeGoals.length,
) )
: 0}% : 0}
%
</Text> </Text>
<Text style={styles.statLabel}>Avg Progress</Text> <Text style={styles.statLabel}>Avg Progress</Text>
</View> </View>
@ -152,7 +244,9 @@ export default function GoalsScreen() {
</ScrollView> </ScrollView>
{/* Floating Action Button */} {/* Floating Action Button */}
<Animated.View style={[styles.fabContainer, { transform: [{ scale: fabScale }] }]}> <Animated.View
style={[styles.fabContainer, { transform: [{ scale: fabScale }] }]}
>
<TouchableOpacity <TouchableOpacity
onPress={() => setIsModalVisible(true)} onPress={() => setIsModalVisible(true)}
onPressIn={() => { onPressIn={() => {
@ -210,8 +304,16 @@ const styles = StyleSheet.create({
borderBottomLeftRadius: theme.borderRadius.xl, borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl, borderBottomRightRadius: theme.borderRadius.xl,
}, },
headerContent: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
debugButton: {
padding: 8,
},
headerTitle: { headerTitle: {
fontSize: theme.typography.fontSize['3xl'], fontSize: theme.typography.fontSize["3xl"],
fontWeight: theme.typography.fontWeight.bold, fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white, color: theme.colors.white,
}, },
@ -230,13 +332,13 @@ const styles = StyleSheet.create({
backgroundColor: theme.colors.white, backgroundColor: theme.colors.white,
padding: 16, padding: 16,
borderRadius: theme.borderRadius.xl, borderRadius: theme.borderRadius.xl,
alignItems: 'center', alignItems: "center",
...theme.shadows.medium, ...theme.shadows.medium,
borderWidth: 1, borderWidth: 1,
borderColor: "rgba(59, 130, 246, 0.1)", borderColor: "rgba(59, 130, 246, 0.1)",
}, },
statValue: { statValue: {
fontSize: theme.typography.fontSize['2xl'], fontSize: theme.typography.fontSize["2xl"],
fontWeight: theme.typography.fontWeight.bold, fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.primary, color: theme.colors.primary,
marginBottom: 4, marginBottom: 4,

View File

@ -2,12 +2,26 @@ import { ClerkProvider, ClerkLoaded } from "@clerk/clerk-expo";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import * as SecureStore from "expo-secure-store"; import * as SecureStore from "expo-secure-store";
import { View, Text } from "react-native"; 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 // Token cache for Clerk
const tokenCache = { const tokenCache = {
async getToken(key: string) { async getToken(key: string) {
try { 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) { } catch (err) {
console.error("Error getting token:", err); console.error("Error getting token:", err);
return null; return null;
@ -15,6 +29,10 @@ const tokenCache = {
}, },
async saveToken(key: string, value: string) { async saveToken(key: string, value: string) {
try { 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); return SecureStore.setItemAsync(key, value);
} catch (err) { } catch (err) {
console.error("Error saving token:", err); console.error("Error saving token:", err);
@ -24,8 +42,71 @@ const tokenCache = {
export default function RootLayout() { export default function RootLayout() {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY; 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) { if (!publishableKey) {
console.log("❌ [RootLayout] No publishable key found!");
return ( return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Missing Clerk Publishable Key</Text> <Text>Missing Clerk Publishable Key</Text>
@ -38,6 +119,23 @@ export default function RootLayout() {
); );
} }
if (!cacheCleared) {
console.log("⏳ [RootLayout] Waiting for cache to clear...");
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Clearing old authentication cache...</Text>
<Text style={{ marginTop: 8, fontSize: 12, color: "#666" }}>
Check the terminal for logs...
</Text>
</View>
);
}
console.log(
"🚀 [RootLayout] Rendering ClerkProvider with key:",
publishableKey?.substring(0, 20) + "...",
);
return ( return (
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}> <ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
<ClerkLoaded> <ClerkLoaded>