From a490292bf6d6d995ae62b4cb034508ec1a996fad Mon Sep 17 00:00:00 2001 From: Valkyriezz Date: Tue, 5 May 2026 02:06:56 +0530 Subject: [PATCH 1/2] fix(db): make MongoDB connection lazy Previously lib/mongodb.ts threw at module load if MONGODB_URI was missing, which broke `next build` even for routes that never touch the database (Next collects page data for all routes at build time, including static ones). Now the URI is only validated when connectDB() is actually called. Build succeeds without env vars; runtime requests that need Mongo fail with a clear error. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/mongodb.ts | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/mongodb.ts b/lib/mongodb.ts index 06ac76c..640aaa5 100644 --- a/lib/mongodb.ts +++ b/lib/mongodb.ts @@ -1,22 +1,33 @@ import mongoose from "mongoose"; -const DB_URI = process.env.MONGODB_URI as string; - -if (!DB_URI) { - throw new Error("❌ MONGODB_URI missing from .env.local"); -} - -export default async function connectDB() { +/** + * Lazy MongoDB connection. + * + * The previous version threw at module load if MONGODB_URI was missing, + * which broke `next build` whenever env vars weren't fully configured β€” + * even for pages that never actually touch the database (Next collects + * page data for every route at build time). Now the URI is validated only + * when a route actually calls connectDB(); build always succeeds, and + * runtime requests that need Mongo fail with a clear, contextual error. + */ +export default async function connectDB(): Promise { if (mongoose.connection.readyState >= 1) { - // 1 = connected, 2 = connecting + // 1 = connected, 2 = connecting β€” already wired up. return; } + const uri = process.env.MONGODB_URI; + if (!uri) { + throw new Error( + "MONGODB_URI is not set. Configure it in .env.local (local) or in your hosting environment (production)." + ); + } + try { - await mongoose.connect(DB_URI, { - bufferCommands: false, - }); - console.log("πŸ“¦ MongoDB connected via Mongoose"); + await mongoose.connect(uri, { bufferCommands: false }); + if (process.env.NODE_ENV !== "production") { + console.log("πŸ“¦ MongoDB connected via Mongoose"); + } } catch (err) { console.error("❌ MongoDB connection error:", err); throw err; From 23b26cf17752bdcb5c0edff81edffcfe2278f10d Mon Sep 17 00:00:00 2001 From: Valkyriezz Date: Tue, 5 May 2026 02:07:54 +0530 Subject: [PATCH 2/2] refactor: consolidate auth onto Firebase, remove Appwrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Appwrite was running in parallel with Firebase as a second auth+storage provider. The dual-source-of-truth caused real production pain β€” Appwrite Cloud's free tier auto-pauses after inactivity, which surfaced as a wall of red 403 errors in DevTools on every page (AuthGuard, Dashboard, ProfileSection, Forums all called authservice.checkUser() on mount). This commit consolidates onto Firebase as the single auth provider and removes Appwrite end-to-end: Auth layer - app/appwrite/auth.ts: rewritten as a Firebase-backed implementation that preserves the existing AuthServiceContract shape ($id, name, email, emailVerification). All callers (login, signup, forgot/reset password, AuthGuard, Navbar, ProfileSection, Forums, Dashboard, PersonalTODO, community) work unchanged. - app/Dashboard/page.tsx, app/components/PersonalTODO.tsx: switch from `account.get()` to `authservice.checkUser()`. - app/community/page.tsx: replace appwrite Models import with the locally-defined AuthUser type. Data layer (clan feature) - app/components/CommunityConnect.tsx: replaced ~300-line clan create/join/leave UI (called Appwrite Databases directly) with a placeholder card. Clan storage was never migrated to Mongo with the rest of the data model and the standalone Express clan server is not deployed, so the feature was already non-functional in production. - Deleted app/api/clan/* (5 routes) β€” only callers were the now-stub CommunityConnect component. Dead code - app/api/APIDebbuger.tsx: a 200-line in-app Appwrite JWT debugger component that no other file imported. - app/api/dev/users/* + app/dev/users/page.tsx: admin-only Appwrite user listing that's not part of any user-facing flow. - app/api/user/{solved-questions,mark-solved}/*: Appwrite-JWT-verifying routes that no page in the app actually called. Dependencies - Removed `appwrite` and `node-appwrite` from package.json. - lib/appwrite.ts (the Client/Account/Databases client) deleted. Net: βˆ’1265 lines, +175. One auth source. No more 403 console wall on auth-gated pages. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/Dashboard/page.tsx | 5 +- app/api/APIDebbuger.tsx | 206 ------------- app/api/clan/[clanId]/kick/route.ts | 39 --- app/api/clan/[clanId]/route.ts | 41 --- app/api/clan/join/route.ts | 39 --- app/api/clan/leave/route.ts | 39 --- app/api/clan/route.ts | 66 ----- app/api/dev/users/[userId]/status/route.ts | 23 -- app/api/dev/users/route.ts | 19 -- app/api/user/mark-solved/route.ts | 56 ---- app/api/user/solved-questions/route.ts | 130 -------- app/appwrite/auth.ts | 278 ++++++++---------- app/community/page.tsx | 6 +- app/components/CommunityConnect.tsx | 326 ++------------------- app/components/PersonalTODO.tsx | 5 +- app/dev/users/page.tsx | 88 ------ lib/appwrite.ts | 14 - package-lock.json | 23 -- package.json | 2 - 19 files changed, 152 insertions(+), 1253 deletions(-) delete mode 100644 app/api/APIDebbuger.tsx delete mode 100644 app/api/clan/[clanId]/kick/route.ts delete mode 100644 app/api/clan/[clanId]/route.ts delete mode 100644 app/api/clan/join/route.ts delete mode 100644 app/api/clan/leave/route.ts delete mode 100644 app/api/clan/route.ts delete mode 100644 app/api/dev/users/[userId]/status/route.ts delete mode 100644 app/api/dev/users/route.ts delete mode 100644 app/api/user/mark-solved/route.ts delete mode 100644 app/api/user/solved-questions/route.ts delete mode 100644 app/dev/users/page.tsx delete mode 100644 lib/appwrite.ts diff --git a/app/Dashboard/page.tsx b/app/Dashboard/page.tsx index 55543c9..717b733 100644 --- a/app/Dashboard/page.tsx +++ b/app/Dashboard/page.tsx @@ -8,16 +8,15 @@ import Navbar from "../components/Navbar"; import PersonalTODO from "../components/PersonalTODO"; import CommunityConnect from "../components/CommunityConnect"; -import { account } from "@/lib/appwrite"; +import authservice from "../appwrite/auth"; const Dashboard = () => { useEffect(() => { const fetchUser = async () => { try { - await account.get(); // βœ… No JWT needed, session maintained + await authservice.checkUser(); } catch (error) { console.error("Not authenticated", error); - // Optionally redirect to login page here } }; diff --git a/app/api/APIDebbuger.tsx b/app/api/APIDebbuger.tsx deleted file mode 100644 index 4d0faff..0000000 --- a/app/api/APIDebbuger.tsx +++ /dev/null @@ -1,206 +0,0 @@ -"use client"; -import { useState } from "react"; -import { account } from "@/lib/appwrite"; - -// Result type for endpoint (success) -type EndpointResult = { - status: number; - statusText: string; - headers: Record; - rawResponse: string; - parsedResponse: unknown; - timestamp: string; -}; - -// Result type for endpoint (error) -type EndpointError = { - error: string; - timestamp: string; -}; - -// Result type for auth success -type AuthResult = { - success: true; - userId: string; - email: string; - jwtLength: number; - timestamp: string; -}; - -// Result type for auth error (re-uses EndpointError) -type ResultsMap = Record; - -const APIDebugger = () => { - const [results, setResults] = useState({}); - const [loading, setLoading] = useState(null); - - const testEndpoint = async ( - endpoint: string, - method: "GET" | "POST" = "GET", - body?: object - ) => { - setLoading(endpoint); - try { - const jwt = await account.createJWT(); - - const options: RequestInit = { - method, - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${jwt.jwt}`, - }, - }; - - if (body) { - options.body = JSON.stringify(body); - } - - console.log(`πŸ§ͺ Testing ${endpoint} with options:`, options); - - const response = await fetch(endpoint, options); - - console.log(`πŸ“‘ Response status: ${response.status}`); - console.log( - `πŸ“‘ Response headers:`, - Object.fromEntries(response.headers.entries()) - ); - - const responseText = await response.text(); - console.log( - `πŸ“„ Raw response (first 1000 chars):`, - responseText.substring(0, 1000) - ); - - let parsedResponse: unknown; - try { - parsedResponse = JSON.parse(responseText); - } catch (e: unknown) { - // Safely handle unknown type with error narrowing - let parseErrorMessage = "Unknown error"; - if (e instanceof Error) parseErrorMessage = e.message; - parsedResponse = { - error: "Failed to parse JSON", - rawResponse: responseText, - parseError: parseErrorMessage, - }; - } - - setResults((prev) => ({ - ...prev, - [endpoint]: { - status: response.status, - statusText: response.statusText, - headers: Object.fromEntries(response.headers.entries()), - rawResponse: responseText, - parsedResponse, - timestamp: new Date().toISOString(), - }, - })); - } catch (error: unknown) { - console.error(`❌ Error testing ${endpoint}:`, error); - // Extract error message safely - let errorMessage = "Unknown error"; - if (error instanceof Error) errorMessage = error.message; - setResults((prev) => ({ - ...prev, - [endpoint]: { - error: errorMessage, - timestamp: new Date().toISOString(), - }, - })); - } finally { - setLoading(null); - } - }; - - const testAuth = async () => { - setLoading("auth"); - try { - const jwt = await account.createJWT(); - const user = await account.get(); - - setResults((prev) => ({ - ...prev, - auth: { - success: true, - userId: user.$id, - email: user.email, - jwtLength: jwt.jwt.length, - timestamp: new Date().toISOString(), - }, - })); - } catch (error: unknown) { - let errorMessage = "Unknown error"; - if (error instanceof Error) errorMessage = error.message; - setResults((prev) => ({ - ...prev, - auth: { - error: errorMessage, - timestamp: new Date().toISOString(), - }, - })); - } finally { - setLoading(null); - } - }; - - return ( -
-

πŸ”§ API Debugger

- -
- - - - - -
- -
- {Object.entries(results).map(([key, result]) => ( -
- - {key} {"error" in result && result.error ? "❌" : "βœ…"} - -
-              {JSON.stringify(result, null, 2)}
-            
-
- ))} -
- - -
- ); -}; - -export default APIDebugger; diff --git a/app/api/clan/[clanId]/kick/route.ts b/app/api/clan/[clanId]/kick/route.ts deleted file mode 100644 index 1fcf0b1..0000000 --- a/app/api/clan/[clanId]/kick/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -// Using the client-side import as requested. -import { databases } from "@/lib/appwrite"; - -const DATABASE_ID = process.env.NEXT_PUBLIC_DATABASE_ID as string; -const PROFILES_COLLECTION_ID = process.env - .NEXT_PUBLIC_PROFILES_COLLECTION_ID as string; - -// POST to kick a member from a clan -export async function POST(request: NextRequest) { - try { - const { userIdToKick } = await request.json(); - - if (!userIdToKick) { - return NextResponse.json( - { message: "User ID to kick is required" }, - { status: 400 } - ); - } - - // ⚠️ This may fail if your Appwrite instance isn’t authenticated with admin permissions - await databases.updateDocument( - DATABASE_ID, - PROFILES_COLLECTION_ID, - userIdToKick, - { clanId: null } - ); - - return NextResponse.json({ message: "Successfully kicked user" }); - } catch (error: unknown) { - const errMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - - return NextResponse.json( - { message: "Failed to kick user", error: errMessage }, - { status: 500 } - ); - } -} diff --git a/app/api/clan/[clanId]/route.ts b/app/api/clan/[clanId]/route.ts deleted file mode 100644 index fea0565..0000000 --- a/app/api/clan/[clanId]/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -// Using the client-side import as requested. -import { databases } from "@/lib/appwrite"; - -const DATABASE_ID = process.env.NEXT_PUBLIC_DATABASE_ID as string; -const CLANS_COLLECTION_ID = process.env - .NEXT_PUBLIC_CLANS_COLLECTION_ID as string; - -// GET information for a specific clan -export async function GET( - request: NextRequest, - { params }: { params: { clanId: string } } -) { - try { - const { clanId } = params; - - if (!clanId) { - return NextResponse.json( - { message: "Clan ID is required" }, - { status: 400 } - ); - } - - // ⚠️ May fail if your collection does not allow read access - const clanData = await databases.getDocument( - DATABASE_ID, - CLANS_COLLECTION_ID, - clanId - ); - - return NextResponse.json(clanData); - } catch (error: unknown) { - const errMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - - return NextResponse.json( - { message: "Failed to fetch clan data", error: errMessage }, - { status: 500 } - ); - } -} diff --git a/app/api/clan/join/route.ts b/app/api/clan/join/route.ts deleted file mode 100644 index e6a0fe8..0000000 --- a/app/api/clan/join/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -// Using the client-side import as requested. -import { databases } from "@/lib/appwrite"; - -const DATABASE_ID = process.env.NEXT_PUBLIC_DATABASE_ID as string; -const PROFILES_COLLECTION_ID = process.env - .NEXT_PUBLIC_PROFILES_COLLECTION_ID as string; - -// POST to join a clan -export async function POST(request: NextRequest) { - try { - const { userId, clanId } = await request.json(); - - if (!userId || !clanId) { - return NextResponse.json( - { message: "User ID and Clan ID are required" }, - { status: 400 } - ); - } - - // ⚠️ This may fail if the Appwrite instance is not authenticated - await databases.updateDocument( - DATABASE_ID, - PROFILES_COLLECTION_ID, - userId, - { clanId } - ); - - return NextResponse.json({ message: "Successfully joined clan" }); - } catch (error: unknown) { - const errMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - - return NextResponse.json( - { message: "Failed to join clan", error: errMessage }, - { status: 500 } - ); - } -} diff --git a/app/api/clan/leave/route.ts b/app/api/clan/leave/route.ts deleted file mode 100644 index 72ab429..0000000 --- a/app/api/clan/leave/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -// Using the client-side import as requested. -import { databases } from "@/lib/appwrite"; - -const DATABASE_ID = process.env.NEXT_PUBLIC_DATABASE_ID as string; -const PROFILES_COLLECTION_ID = process.env - .NEXT_PUBLIC_PROFILES_COLLECTION_ID as string; - -// POST to leave a clan -export async function POST(request: NextRequest) { - try { - const { userId } = await request.json(); - - if (!userId) { - return NextResponse.json( - { message: "User ID is required" }, - { status: 400 } - ); - } - - // ⚠️ This may fail with 401 Unauthorized unless Appwrite is authenticated - await databases.updateDocument( - DATABASE_ID, - PROFILES_COLLECTION_ID, - userId, - { clanId: null } - ); - - return NextResponse.json({ message: "Successfully left clan" }); - } catch (error: unknown) { - const errMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - - return NextResponse.json( - { message: "Failed to leave clan", error: errMessage }, - { status: 500 } - ); - } -} diff --git a/app/api/clan/route.ts b/app/api/clan/route.ts deleted file mode 100644 index 7f5cdab..0000000 --- a/app/api/clan/route.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getAppwriteDatabases } from "@/lib/appwrite"; // server-side client - -const DATABASE_ID = process.env.NEXT_PUBLIC_DATABASE_ID as string; -const CLANS_COLLECTION_ID = process.env - .NEXT_PUBLIC_CLANS_COLLECTION_ID as string; -const PROFILES_COLLECTION_ID = process.env - .NEXT_PUBLIC_PROFILES_COLLECTION_ID as string; - -// GET the current user's clan -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const userId = searchParams.get("userId"); - - if (!userId) { - return NextResponse.json( - { message: "User ID is required" }, - { status: 400 } - ); - } - - const databases = getAppwriteDatabases(); - - const profile = await databases.getDocument( - DATABASE_ID, - PROFILES_COLLECTION_ID, - userId - ); - - if (!profile.clanId) { - return NextResponse.json( - { message: "User not in a clan" }, - { status: 404 } - ); - } - - const clanData = await databases.getDocument( - DATABASE_ID, - CLANS_COLLECTION_ID, - profile.clanId as string - ); - - return NextResponse.json(clanData); - } catch (error: unknown) { - if ( - error && - typeof error === "object" && - "code" in error && - (error as any).code === 404 - ) { - return NextResponse.json( - { message: "User not in a clan" }, - { status: 404 } - ); - } - - const errMessage = - error instanceof Error ? error.message : "Unknown server error"; - - return NextResponse.json( - { message: "Server error", error: errMessage }, - { status: 500 } - ); - } -} diff --git a/app/api/dev/users/[userId]/status/route.ts b/app/api/dev/users/[userId]/status/route.ts deleted file mode 100644 index 198e0d8..0000000 --- a/app/api/dev/users/[userId]/status/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextResponse } from 'next/server'; -import { Client, Users } from 'node-appwrite'; - -export async function PUT(req: Request, { params }: { params: Promise<{ userId: string }> }) { - const { userId } = await params; - const body = await req.json(); - const { status } = body; // e.g., 'active', 'banned' - - try { - const client = new Client() - .setEndpoint(process.env.APPWRITE_ENDPOINT!) - .setProject(process.env.APPWRITE_PROJECT_ID!) - .setKey(process.env.APPWRITE_API_KEY!); - - const users = new Users(client); - await users.updatePrefs(userId, { status }); - - return NextResponse.json({ message: 'Status updated' }); - } catch (error) { - console.error("Failed to update status:", error); - return NextResponse.json({ error: 'Failed to update status' }, { status: 500 }); - } -} \ No newline at end of file diff --git a/app/api/dev/users/route.ts b/app/api/dev/users/route.ts deleted file mode 100644 index 3febf11..0000000 --- a/app/api/dev/users/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NextResponse } from 'next/server'; -import { Client, Users } from 'node-appwrite'; - -export async function GET() { - try { - const client = new Client() - .setEndpoint(process.env.APPWRITE_ENDPOINT!) // example: 'https://cloud.appwrite.io/v1' - .setProject(process.env.APPWRITE_PROJECT_ID!) - .setKey(process.env.APPWRITE_API_KEY!); // secret API key - - const users = new Users(client); - const allUsers = await users.list(); - - return NextResponse.json(allUsers.users); - } catch (error) { - console.error("Failed to fetch Appwrite users:", error); - return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 }); - } -} diff --git a/app/api/user/mark-solved/route.ts b/app/api/user/mark-solved/route.ts deleted file mode 100644 index 3fd3700..0000000 --- a/app/api/user/mark-solved/route.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; -import connectDB from "@/lib/mongodb"; -import UserModel from "@/models/Users"; - - - -// POST /api/user/mark-solved -export async function POST(req: NextRequest) { - try { - const authHeader = req.headers.get("authorization"); - if (!authHeader) return NextResponse.json({ error: "No auth token" }, { status: 401 }); - - const token = authHeader.replace("Bearer ", "").trim(); - if (!token) return NextResponse.json({ error: "Invalid auth token" }, { status: 401 }); - - const verifyResponse = await fetch( - `${process.env.NEXT_PUBLIC_API_ENDPOINT}/account/sessions/jwt/verify`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Appwrite-Project": process.env.NEXT_PUBLIC_PROJECT_ID!, - "X-Appwrite-Key": process.env.APPWRITE_API_KEY!, - }, - body: JSON.stringify({ jwt: token }), - } - ); - - if (!verifyResponse.ok) { - return NextResponse.json({ error: "Invalid token" }, { status: 401 }); - } - const verifyData = await verifyResponse.json(); - - await connectDB(); - const user = await UserModel.findOne({ appwriteId: verifyData.userId }); - if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 }); - - const body = await req.json(); - const questionId = body.questionId; - if (!questionId || typeof questionId !== "string") { - return NextResponse.json({ error: "Invalid questionId" }, { status: 400 }); - } - - if (!user.solvedQuestions) user.solvedQuestions = []; - if (!user.solvedQuestions.includes(questionId)) { - user.solvedQuestions.push(questionId); - await user.save(); - } - - return NextResponse.json({ success: true }); - } catch (error) { - console.error("Error marking solved question with Appwrite auth:", error); - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } -} diff --git a/app/api/user/solved-questions/route.ts b/app/api/user/solved-questions/route.ts deleted file mode 100644 index 4743917..0000000 --- a/app/api/user/solved-questions/route.ts +++ /dev/null @@ -1,130 +0,0 @@ -// /app/api/user/solved-questions/route.ts - -import { NextResponse } from "next/server"; -import connectDB from "@/lib/mongodb"; -import User from "@/models/Users"; -import Questions from "@/models/Questions"; - -// Interface for the JWT verification response from Appwrite -interface AppwriteJWTVerifyResponse { - userId: string; - // Avoid index signature with any; add fields here if needed -} - -// Computed type for the solved questions data by category -type SolvedQuestionCategory = { - name: string; - questionCount: number; - progress: number; // Percentage solved in category - questions: string[]; -}; - -// Helper: Verify Appwrite JWT token via Appwrite REST API -async function verifyAppwriteJWT(token: string): Promise { - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_ENDPOINT}/v1/account/sessions/jwt/verify`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Appwrite-Project": process.env.NEXT_PUBLIC_PROJECT_ID!, - "X-Appwrite-Key": process.env.APPWRITE_API_KEY!, // Server API key (keep secret!) - }, - body: JSON.stringify({ jwt: token }), - } - ); - if (!res.ok) { - throw new Error("Invalid or expired token"); - } - - const verified: AppwriteJWTVerifyResponse = await res.json(); - - if (!verified.userId || typeof verified.userId !== "string") { - throw new Error("Invalid token response structure"); - } - - return verified; -} - -export async function GET(request: Request) { - try { - const authHeader = request.headers.get("authorization"); - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return NextResponse.json({ error: "Missing or invalid auth token" }, { status: 401 }); - } - const token = authHeader.substring(7); - - // Verify token & get user ID - const verified = await verifyAppwriteJWT(token); - const appwriteUserId = verified.userId; - if (!appwriteUserId) { - return NextResponse.json({ error: "User not found in token" }, { status: 401 }); - } - - // Connect to DB - await connectDB(); - - // Find user by Appwrite ID - const user = await User.findOne({ appwriteId: appwriteUserId }); - if (!user) { - return NextResponse.json({ error: "User not found" }, { status: 404 }); - } - - const solvedIds = user.solvedQuestions || []; - - // Fetch solved questions data - const solvedQuestions = await Questions.find({ _id: { $in: solvedIds } }).lean(); - - // Fetch all questions for progress calculation - const allQuestions = await Questions.find({}).lean(); - - // Calculate totals per category - const totalByCategory: Record = {}; - allQuestions.forEach((q) => { - if (q.category && typeof q.category === "string") { - totalByCategory[q.category] = (totalByCategory[q.category] || 0) + 1; - } - }); - - // Group solved questions per category - const categoryMap: Record< - string, - { questions: string[]; totalQuestions: number } - > = {}; - - solvedQuestions.forEach((q) => { - if (!q.category || typeof q.category !== "string") return; // skip if no category or invalid - if (!categoryMap[q.category]) { - categoryMap[q.category] = { - questions: [], - totalQuestions: totalByCategory[q.category] || 0, - }; - } - categoryMap[q.category].questions.push(q.title || "Untitled"); - }); - - // Format the output - const solvedQuestionsData: SolvedQuestionCategory[] = Object.entries(categoryMap).map( - ([category, { questions, totalQuestions }]) => ({ - name: category, - questionCount: questions.length, - progress: totalQuestions ? Math.round((questions.length / totalQuestions) * 100) : 0, - questions, - }) - ); - - return NextResponse.json({ success: true, solvedQuestions: solvedQuestionsData }); - } catch (error: unknown) { - console.error("[GET] /api/user/solved-questions error:", error); - - const message = - error instanceof Error - ? error.message - : "Internal server error"; - - return NextResponse.json( - { error: message }, - { status: 500 } - ); - } -} diff --git a/app/appwrite/auth.ts b/app/appwrite/auth.ts index 9d785f0..251697f 100644 --- a/app/appwrite/auth.ts +++ b/app/appwrite/auth.ts @@ -1,203 +1,167 @@ -import { Client, Account, ID, Models, AppwriteException } from "appwrite"; +/** + * Auth service β€” Firebase-backed. + * + * Historically this wrapped Appwrite. We've consolidated auth onto Firebase + * (one provider, no auto-pause, no dual-source-of-truth bugs). The exported + * shape is preserved so callers in this repo keep working: `$id`, `name`, + * `email`, `emailVerification` are populated from the Firebase user. + * + * The folder name `app/appwrite/` is kept only to avoid an import-path churn + * across the codebase. Treat it as the "auth" folder. + */ + +import { app } from "@/lib/firebase"; +import { + getAuth, + createUserWithEmailAndPassword, + signInWithEmailAndPassword, + signOut, + onAuthStateChanged, + sendPasswordResetEmail, + confirmPasswordReset, + updateProfile, + type User as FirebaseUser, +} from "firebase/auth"; + +const auth = getAuth(app); /** - * Contract every auth service must fulfill. - * Uses the exact return types the v12 SDK gives us. + * Public shape, preserved for backwards compatibility with callers that were + * written against Appwrite's `Models.User`. */ +export interface AuthUser { + $id: string; + name: string; + email: string; + emailVerification: boolean; + /** Some legacy callers spread or read from `prefs`; keep it as an empty object. */ + prefs: Record; +} + +/** Lightweight session marker β€” callers only check truthiness. */ +export interface AuthSession { + $id: string; + userId: string; +} -export const accountInstance = new Account( - new Client() - .setEndpoint(process.env.NEXT_PUBLIC_API_ENDPOINT as string) - .setProject(process.env.NEXT_PUBLIC_PROJECT_ID as string) -); export interface AuthServiceContract { - signUp( - email: string, - password: string, - name: string - ): Promise>; - signIn(email: string, password: string): Promise; + signUp(email: string, password: string, name: string): Promise; + signIn(email: string, password: string): Promise; logout(): Promise<{ success: boolean; error?: string }>; - checkUser(): Promise | null>; + checkUser(): Promise; sendPasswordReset(email: string): Promise; isAuthenticated(): Promise; createSocialUser( email: string, name: string, uid: string - ): Promise>; + ): Promise; + /** + * For Firebase, the password reset URL carries an `oobCode` rather than + * Appwrite's `userId`+`secret`. The first arg is unused β€” the second arg is + * the oobCode from the URL β€” and the third is the new password. + */ updatePassword( userId: string, - secret: string, + secretOrOobCode: string, newPassword: string - ): Promise; + ): Promise<{ $id: string }>; } -/** - * Concrete Appwrite implementation. - */ -class AppwriteAuthService implements AuthServiceContract { - private client: Client; - private account: Account; - private readonly resetRedirect = `${process.env.NEXT_PUBLIC_APP_URL}/resetPassword`; - - constructor() { - this.client = new Client() - .setEndpoint(process.env.NEXT_PUBLIC_API_ENDPOINT as string) - .setProject(process.env.NEXT_PUBLIC_PROJECT_ID as string); - - this.account = new Account(this.client); - } +function fromFirebaseUser(u: FirebaseUser): AuthUser { + return { + $id: u.uid, + name: u.displayName || u.email?.split("@")[0] || "User", + email: u.email || "", + emailVerification: u.emailVerified, + prefs: {}, + }; +} - async signUp(email: string, password: string, name: string) { - try { - return await this.account.create(ID.unique(), email, password, name); - } catch (error) { - if (error instanceof AppwriteException) { - throw new Error(`Sign up failed: ${error.message}`); +class FirebaseAuthService implements AuthServiceContract { + async signUp(email: string, password: string, name: string): Promise { + const cred = await createUserWithEmailAndPassword(auth, email, password); + if (name && cred.user) { + try { + await updateProfile(cred.user, { displayName: name }); + } catch { + // Non-fatal β€” we have a user, just no displayName persisted. } - throw error; } + return fromFirebaseUser(cred.user); } - async signIn(email: string, password: string) { - try { - return await this.account.createEmailPasswordSession(email, password); - } catch (error) { - if (error instanceof AppwriteException) { - throw new Error(`Sign in failed: ${error.message}`); - } - throw error; - } + async signIn(email: string, password: string): Promise { + const cred = await signInWithEmailAndPassword(auth, email, password); + return { $id: cred.user.uid, userId: cred.user.uid }; } async logout(): Promise<{ success: boolean; error?: string }> { try { - // Attempt to delete current session - await this.account.deleteSession("current"); + await signOut(auth); return { success: true }; } catch (error) { - // If unauthorized, treat as success (session already deleted) - if (error instanceof AppwriteException && error.code === 401) { - return { success: true }; - } - - // Fallback: delete all sessions - try { - await this.account.deleteSessions(); - return { success: true }; - } catch (fallbackError) { - if ( - fallbackError instanceof AppwriteException && - fallbackError.code === 401 - ) { - return { success: true }; - } - return { - success: false, - error: - fallbackError instanceof Error - ? fallbackError.message - : "Unknown logout error", - }; - } + return { + success: false, + error: error instanceof Error ? error.message : "Unknown logout error", + }; } } - async checkUser(): Promise | null> { - try { - return await this.account.get(); - } catch (error) { - if ( - error instanceof AppwriteException && - (error.code === 401 || error.message.includes("missing scope")) - ) { - return null; - } - throw error; - } + /** + * Resolves the current user. Waits for the first `onAuthStateChanged` event + * so a freshly-loaded page doesn't return null while Firebase rehydrates + * the session from local storage. + */ + checkUser(): Promise { + return new Promise((resolve) => { + const unsubscribe = onAuthStateChanged( + auth, + (user) => { + unsubscribe(); + resolve(user ? fromFirebaseUser(user) : null); + }, + () => { + unsubscribe(); + resolve(null); + } + ); + }); } async isAuthenticated(): Promise { - try { - const user = await this.checkUser(); - return user !== null; - } catch { - return false; - } + return (await this.checkUser()) !== null; } - async sendPasswordReset(email: string) { - try { - await this.account.createRecovery(email, this.resetRedirect); - } catch (error) { - if (error instanceof AppwriteException) { - throw new Error(`Password reset failed: ${error.message}`); - } - throw error; - } + async sendPasswordReset(email: string): Promise { + await sendPasswordResetEmail(auth, email); } async updatePassword( - userId: string, - secret: string, + _userId: string, + oobCode: string, newPassword: string - ): Promise { - try { - return await this.account.updateRecovery(userId, secret, newPassword); - } catch (error) { - if (error instanceof AppwriteException) { - throw new Error(`Password update failed: ${error.message}`); - } - throw error; - } + ): Promise<{ $id: string }> { + await confirmPasswordReset(auth, oobCode, newPassword); + return { $id: oobCode }; } - async createSocialUser( - email: string, - name: string, - uid: string - ): Promise> { - try { - const existingUser = await this.checkUser(); - if (existingUser) return existingUser; - - // Use a crypto-polyfill or fallback if crypto.randomUUID() is not available - const generateUUID = () => { - if (typeof crypto !== "undefined" && "randomUUID" in crypto) { - return crypto.randomUUID(); - } else { - // fallback UUID v4 generator - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( - /[xy]/g, - (c) => { - const r = (Math.random() * 16) | 0; - const v = c === "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - } - ); - } - }; - - const password = generateUUID(); - - // IMPORTANT: Make sure `uid` value complies with Appwrite user ID requirements - // It should be a string with allowed characters and length <= 36. - - const newUser = await this.account.create(uid, email, password, name); - - // Auto-login by creating session for the new user - await this.account.createEmailPasswordSession(email, password); - - return newUser; - } catch (error) { - console.error("Error creating social user:", error); - throw error; - } + /** + * For social logins: Firebase already populated `auth.currentUser` after + * `signInWithPopup`. Just return it (or wait for it via onAuthStateChanged). + * The legacy Appwrite implementation created a parallel Appwrite user with + * the social user's email β€” we no longer need that bookkeeping. + */ + // The Appwrite-era signature carried email/name/uid because we had to mirror + // the Firebase user into Appwrite. Firebase already has the user after + // signInWithPopup, so we just resolve whatever Firebase says is current. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async createSocialUser(_email: string, _name: string, _uid: string): Promise { + const u = await this.checkUser(); + if (u) return u; + throw new Error("Social sign-in did not produce a Firebase user"); } } -/* Export singleton */ -const authservice: AuthServiceContract = new AppwriteAuthService(); - +const authservice: AuthServiceContract = new FirebaseAuthService(); export default authservice; diff --git a/app/community/page.tsx b/app/community/page.tsx index b74ec45..e3c9365 100644 --- a/app/community/page.tsx +++ b/app/community/page.tsx @@ -4,14 +4,12 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import ChatWindow from "../components/ChatWindow"; -import authservice from "../appwrite/auth"; -import type { Models } from "appwrite"; +import authservice, { type AuthUser } from "../appwrite/auth"; import Navbar from "../components/Navbar"; export default function CommunityPage() { const router = useRouter(); - const [session, setSession] = - useState | null>(null); + const [session, setSession] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { diff --git a/app/components/CommunityConnect.tsx b/app/components/CommunityConnect.tsx index 6ebd9be..e1f2661 100644 --- a/app/components/CommunityConnect.tsx +++ b/app/components/CommunityConnect.tsx @@ -1,306 +1,28 @@ "use client"; -import React, { useState, useEffect } from "react"; -import { motion } from "framer-motion"; -import { Tab } from "@headlessui/react"; -import { cn } from "@/lib/utils"; -import { account, databases, ID } from "@/lib/appwrite"; -import { AppwriteException } from "appwrite"; -import { Loader2, AlertTriangle, LogOut } from "lucide-react"; // For better icons - -// Clan interface matching your Appwrite collection -interface Clan { - $id: string; - name: string; - tag: string; - memberCount: number; -} - -// Environment variables -const DATABASE_ID = process.env.NEXT_PUBLIC_DATABASE_ID as string; -const CLANS_COLLECTION_ID = process.env - .NEXT_PUBLIC_CLANS_COLLECTION_ID as string; -const PROFILES_COLLECTION_ID = process.env - .NEXT_PUBLIC_PROFILES_COLLECTION_ID as string; - -const CommunityConnect: React.FC = () => { - const [myClan, setMyClan] = useState(null); - const [joinKey, setJoinKey] = useState(""); - const [newClanName, setNewClanName] = useState(""); - const [userId, setUserId] = useState(null); // Store user ID - - const [isLoading, setIsLoading] = useState(true); - const [isSubmitting, setIsSubmitting] = useState(false); // For form submissions - const [error, setError] = useState(null); - - // Fetch initial user and clan data - useEffect(() => { - const fetchUserClanData = async () => { - try { - const currentUser = await account.get(); - setUserId(currentUser.$id); // Store the user ID - - const profile = await databases.getDocument( - DATABASE_ID, - PROFILES_COLLECTION_ID, - currentUser.$id - ); - - if (profile.clanId) { - const clanData = await databases.getDocument( - DATABASE_ID, - CLANS_COLLECTION_ID, - profile.clanId as string - ); - setMyClan(clanData as unknown as Clan); - } - } catch (err) { - // This catch block is for genuine errors, not for users who aren't in a clan. - if (err instanceof AppwriteException && err.code !== 404) { - console.error("Failed to fetch user clan data:", err); - setError( - "Could not load your community information. Please try refreshing." - ); - } - } finally { - setIsLoading(false); - } - }; - - fetchUserClanData(); - }, []); - - // --- API HANDLERS --- - - const handleJoinClan = async () => { - if (!joinKey.trim() || !userId) return; - setIsSubmitting(true); - setError(null); - - try { - await databases.updateDocument( - DATABASE_ID, - PROFILES_COLLECTION_ID, - userId, - { clanId: joinKey.trim() } - ); - const clanData = await databases.getDocument( - DATABASE_ID, - CLANS_COLLECTION_ID, - joinKey.trim() - ); - setMyClan(clanData as unknown as Clan); - setJoinKey(""); - } catch (err) { - setError( - err instanceof AppwriteException ? err.message : "Failed to join clan." - ); - } finally { - setIsSubmitting(false); - } - }; - - const handleCreateClan = async () => { - if (!newClanName.trim() || !userId) return; - setIsSubmitting(true); - setError(null); - - try { - const tag = newClanName.trim().substring(0, 4).toUpperCase(); - const newClan = await databases.createDocument( - DATABASE_ID, - CLANS_COLLECTION_ID, - ID.unique(), - { name: newClanName.trim(), tag, memberCount: 1 } - ); - await databases.updateDocument( - DATABASE_ID, - PROFILES_COLLECTION_ID, - userId, - { clanId: newClan.$id } - ); - setMyClan(newClan as unknown as Clan); - setNewClanName(""); - } catch (err) { - setError( - err instanceof AppwriteException - ? err.message - : "Failed to create clan." - ); - } finally { - setIsSubmitting(false); - } - }; - - const handleLeaveClan = async () => { - if (!userId) return; - if (!window.confirm("Are you sure you want to leave this clan?")) return; - setIsSubmitting(true); - setError(null); - - try { - await databases.updateDocument( - DATABASE_ID, - PROFILES_COLLECTION_ID, - userId, - { clanId: null } - ); - setMyClan(null); // Update UI immediately - } catch (err) { - setError( - err instanceof AppwriteException ? err.message : "Failed to leave clan." - ); - } finally { - setIsSubmitting(false); - } - }; - - // --- RENDER STATES --- - - if (isLoading) { - return ( -
-
- -

Loading Community...

-
-
- ); - } - - if (error && !isSubmitting) { - return ( -
- -

An Error Occurred

-

{error}

- -
- ); - } - +/** + * CommunityConnect β€” placeholder. + * + * The clan create/join/leave flow previously hit Appwrite Databases. As part + * of consolidating the auth + storage stack, the clan storage layer is being + * migrated; until that lands, this component renders a card that explains + * the state of the feature instead of calling a backend that no longer + * matches the rest of the app's data model. + */ + +import { Users } from "lucide-react"; + +export default function CommunityConnect() { return ( - -

- Community Connect -

- - - - {["My Clan", "Join Clan", "Create Clan"].map((tab) => ( - cn(/* ...cn styles... */)} - > - {tab} - - ))} - - - - {/* My Clan Panel */} - - {myClan ? ( -
-

- {myClan.name} [{myClan.tag}] -

-

- Members: {myClan.memberCount} -

-

- Clan ID:{" "} - - {myClan.$id} - -

- -
- ) : ( -

- You're not in a clan yet. Join or create one! -

- )} -
- - {/* Join Clan Panel */} - -
- - setJoinKey(e.target.value)} - disabled={isSubmitting} - placeholder="e.g., 65d8c..." - className="w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:border-gray-600 focus:ring-2 focus:ring-green-500" - /> - -
-
- - {/* Create Clan Panel */} - -
- - setNewClanName(e.target.value)} - disabled={isSubmitting} - placeholder="e.g., The Code Crusaders" - className="w-full px-3 py-2 border rounded-md dark:bg-gray-900 dark:border-gray-600 focus:ring-2 focus:ring-purple-500" - /> - -
-
-
- - {error && isSubmitting && ( -

{error}

- )} -
-
+
+ +

+ Clans +

+

+ Group coding squads are being rebuilt on the unified storage layer. + For now, jump into the public forums or the playground. +

+
); -}; - -export default CommunityConnect; +} diff --git a/app/components/PersonalTODO.tsx b/app/components/PersonalTODO.tsx index 0197043..28ed271 100644 --- a/app/components/PersonalTODO.tsx +++ b/app/components/PersonalTODO.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { Plus, Trash2, Check, Clock } from "lucide-react"; -import { account } from "@/lib/appwrite"; +import authservice from "../appwrite/auth"; interface Task { _id: string; @@ -25,7 +25,8 @@ const PersonalTODO = () => { useEffect(() => { const fetchData = async () => { try { - const user = await account.get(); + const user = await authservice.checkUser(); + if (!user) throw new Error("Not authenticated"); setUserId(user.$id); const res = await fetch(`/api/tasks?userId=${user.$id}`); diff --git a/app/dev/users/page.tsx b/app/dev/users/page.tsx deleted file mode 100644 index 13f6c79..0000000 --- a/app/dev/users/page.tsx +++ /dev/null @@ -1,88 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -type User = { - $id: string; - email: string; - name: string; - prefs: { status?: string }; -}; - -export default function UserManagement() { - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - - const fetchUsers = async () => { - const res = await fetch("/api/dev/users"); - const data = await res.json(); - setUsers(data); - setLoading(false); - }; - - const updateStatus = async (userId: string, newStatus: string) => { - const res = await fetch(`/api/dev/users/${userId}/status`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: newStatus }), - }); - if (res.ok) { - fetchUsers(); // Refresh UI - } - }; - - useEffect(() => { - fetchUsers(); - }, []); - - if (loading) return
Loading users...
; - - return ( -
-

User Management

- - - - - - - - - - - {users.map((user: User) => ( - - - - - - - ))} - -
NameEmailStatusActions
{user.name || "N/A"}{user.email} - - {user.prefs?.status || "active"} - - - - -
-
- ); -} diff --git a/lib/appwrite.ts b/lib/appwrite.ts deleted file mode 100644 index ef917a8..0000000 --- a/lib/appwrite.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Client, Account, Databases, ID, Query } from "appwrite"; - -const client = new Client() - .setEndpoint(process.env.NEXT_PUBLIC_API_ENDPOINT as string) - .setProject(process.env.NEXT_PUBLIC_PROJECT_ID as string); - -const account = new Account(client); -const databases = new Databases(client); - -// βœ… helpers -export const getAppwriteClient = () => client; -export const getAppwriteDatabases = () => databases; - -export { client, account, databases, ID, Query }; diff --git a/package-lock.json b/package-lock.json index a2e0c0b..91699ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@tinymce/tinymce-react": "^6.2.1", "ace-builds": "^1.43.0", "aos": "^2.3.4", - "appwrite": "^18.1.1", "axios": "^1.10.0", "bcryptjs": "^3.0.2", "body-parser": "^2.2.0", @@ -32,7 +31,6 @@ "mongoose": "^8.16.3", "next": "15.3.4", "next-themes": "^0.4.6", - "node-appwrite": "^17.2.0", "node-fetch": "^3.3.2", "nprogress": "^0.2.0", "react": "^19.0.0", @@ -3276,12 +3274,6 @@ "lodash.throttle": "^4.0.1" } }, - "node_modules/appwrite": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/appwrite/-/appwrite-18.1.1.tgz", - "integrity": "sha512-krwHjuwJcF+9Ig2+nqOEKMA/5kPIFhwwZsaLc7Gb8y2oP6EnG4ZMRPeHTFscdevOtVQj2Ax92cYYWAEvzlrc7A==", - "license": "BSD-3-Clause" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -8066,15 +8058,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/node-appwrite": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-17.2.0.tgz", - "integrity": "sha512-naYUsVZEdF6YKF/q/s0DBQkAHJj2RhEWwmcXrkbfAAPc3Bn+D3CCftBqyNWeo17JBCoYWqQEgT2Od9xOVQR26A==", - "license": "BSD-3-Clause", - "dependencies": { - "node-fetch-native-with-agent": "1.7.2" - } - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -8113,12 +8096,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/node-fetch-native-with-agent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz", - "integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==", - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", diff --git a/package.json b/package.json index 927e126..3f9c86f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "@tinymce/tinymce-react": "^6.2.1", "ace-builds": "^1.43.0", "aos": "^2.3.4", - "appwrite": "^18.1.1", "axios": "^1.10.0", "bcryptjs": "^3.0.2", "body-parser": "^2.2.0", @@ -33,7 +32,6 @@ "mongoose": "^8.16.3", "next": "15.3.4", "next-themes": "^0.4.6", - "node-appwrite": "^17.2.0", "node-fetch": "^3.3.2", "nprogress": "^0.2.0", "react": "^19.0.0",