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/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; 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",