diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ada1d26 Binary files /dev/null and b/.DS_Store differ diff --git a/client/src/app/admin/page.tsx b/client/src/app/admin/page.tsx index c91ea85..de6545e 100644 --- a/client/src/app/admin/page.tsx +++ b/client/src/app/admin/page.tsx @@ -1,11 +1,16 @@ -//src/app/admin/page.tsx -// Admin dashboard page "use client"; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState } from "react"; import messages from "@/constants/messages"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; -import { Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell } from "@/components/ui/table"; +import { + Table, + TableHead, + TableRow, + TableHeaderCell, + TableBody, + TableCell, +} from "@/components/ui/table"; import { useRouter } from "next/navigation"; interface ApiStat { @@ -15,119 +20,120 @@ interface ApiStat { } interface UserStat { + username: string; email: string; - token: string; totalRequests: number; } const AdminDashboard: React.FC = () => { const [apiStats, setApiStats] = useState([]); - //will be implemented later using new api endpoint eg. ${process.env.NEXT_PUBLIC_API_URL}/api_stats const [userStats, setUserStats] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const router = useRouter(); useEffect(() => { - const checkAuth = async () => { + const fetchData = async () => { try { - const response = await fetch( + // Authenticate admin user + const authResponse = await fetch( `${process.env.NEXT_PUBLIC_USER_DATABASE}/verify-token`, { method: "GET", - credentials: "include", // Include cookies in the request + credentials: "include", } ); - if (response.status !== 200) { - router.push("/login"); - return; - } - const data = await response.json(); - console.log("Verification response:", data.info.role); + if (!authResponse.ok) { + throw new Error("Authentication failed. Please log in."); + } - if (data.info.role !== "admin") { - router.push("/login"); + const authData = await authResponse.json(); + if (authData.info.role !== "admin") { + router.replace("/login"); return; } - // Handle verification response here - } catch (error) { - console.error("Error:", error); - } - }; - - const fetchApiData = async () => { - try { - // Retrieve the JWT token from cookies - // const token = document.cookie - // .split('; ') - // .find(row => row.startsWith('authToken=')) - // ?.split('=')[1]; - - // if (!token) { - // setError(messages.auth.notAuthenticated); - // return; - // } - - // Fetch all user information - const userResponse = await fetch(`${process.env.NEXT_PUBLIC_USER_DATABASE}/users`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + // Fetch API call and user data + const [apiCallsResponse, usersResponse] = await Promise.all([ + fetch(`${process.env.NEXT_PUBLIC_USER_DATABASE}/api-calls`, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }), + fetch(`${process.env.NEXT_PUBLIC_USER_DATABASE}/users`, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }), + ]); - if (!userResponse.ok) { - throw new Error(messages.fetch.userStatsError); + if (!apiCallsResponse.ok || !usersResponse.ok) { + throw new Error("Failed to fetch data."); } - const users = await userResponse.json(); + const apiCalls = await apiCallsResponse.json(); + const users = await usersResponse.json(); - // Fetch API stats for each user - const userStatsPromises = users.map(async (user: { id: string; email: string}) => { - const userStatsResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api_count?user_id=${user.id}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - - if (userStatsResponse.ok) { - const { api_count: totalRequests } = await userStatsResponse.json(); - return { - email: user.email, - totalRequests, - }; + // Aggregate API stats by method and endpoint + const apiStatsMap = new Map(); + apiCalls.forEach((call: { http_method: string; endpoint: string }) => { + const key = `${call.http_method} ${call.endpoint}`; + if (apiStatsMap.has(key)) { + const stat = apiStatsMap.get(key)!; + stat.requests += 1; } else { - console.error(`${messages.fetch.userStatsError} for user ID: ${user.id}`); - return null; + apiStatsMap.set(key, { + method: call.http_method, + endpoint: call.endpoint, + requests: 1, + }); } }); - const userStatsResults = await Promise.all(userStatsPromises); - setUserStats(userStatsResults.filter(Boolean) as UserStat[]); // Remove any null entries - } catch (error) { - if (error instanceof Error) { - setError(error.message); - } else { - setError(messages.fetch.unknownError); - } - console.error(messages.fetch.unknownError, error); + const sortedApiStats = Array.from(apiStatsMap.values()).sort((a, b) => { + if (a.endpoint === b.endpoint) { + return a.method.localeCompare(b.method); + } + return a.endpoint.localeCompare(b.endpoint); + }); + setApiStats(sortedApiStats); + + // Aggregate user stats + const userStatsMap = new Map(); + apiCalls.forEach((call: { user_id: string }) => { + if (userStatsMap.has(call.user_id)) { + userStatsMap.set(call.user_id, userStatsMap.get(call.user_id)! + 1); + } else { + userStatsMap.set(call.user_id, 1); + } + }); + + const userStatsArray: UserStat[] = users.map( + (user: { id: string; email: string; username?: string }) => ({ + username: user.username || `User ID: ${user.id}`, + email: user.email, + totalRequests: userStatsMap.get(user.id) || 0, + }) + ); + + setUserStats(userStatsArray); + } catch (error: any) { + setError(error.message || "An error occurred."); + console.error(error); } finally { setLoading(false); } }; - checkAuth(); - fetchApiData(); + fetchData(); }, []); return (
- + - {messages.dashboard.title} + + {messages.dashboard.title} + {loading ? ( @@ -136,7 +142,10 @@ const AdminDashboard: React.FC = () => {

{error}

) : ( <> -

{messages.dashboard.apiStatsTitle}

+ {/* API Stats Table */} +

+ {messages.dashboard.apiStatsTitle} +

@@ -156,20 +165,23 @@ const AdminDashboard: React.FC = () => { )) ) : ( - + )}
+ {messages.table.noApiStats} -
-

{messages.dashboard.userStatsTitle}

+ {/* User Stats Table */} +

+ {messages.dashboard.userStatsTitle} +

+ Username Email - Token Total Requests @@ -177,16 +189,16 @@ const AdminDashboard: React.FC = () => { {userStats.length ? ( userStats.map((user, index) => ( + {user.username} {user.email} - {user.token} {user.totalRequests} )) ) : ( - + )} diff --git a/client/src/app/dashboard/forgotPassword/page.tsx b/client/src/app/dashboard/forgotPassword/page.tsx new file mode 100644 index 0000000..9d21a45 --- /dev/null +++ b/client/src/app/dashboard/forgotPassword/page.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import Layout from '@/components/Layout'; +import Link from "next/link"; +import { FormEvent, useState } from "react"; + +export default function ForgotPassword() { + const [email, setEmail] = useState(""); + const [message, setMessage] = useState(""); + const [messageType, setMessageType] = useState<"success" | "error" | "">(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setLoading(true); + + // Call API to initiate the password reset + const response = await fetch( + `${process.env.NEXT_PUBLIC_USER_DATABASE}/forgotPassword`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + } + ); + + if (response.ok) { + setMessage("Password reset link sent to your email."); + setMessageType("success"); + } else { + const errorData = await response.json(); + setMessage(`Error: ${errorData.message}`); + setMessageType("error"); + } + + setLoading(false); + }; + + return ( + + +
+

Forgot Password

+
+
+ setEmail(e.target.value)} + /> + + + {message && ( +

+ {message} +

+ )} + +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/app/dashboard/login/page.tsx b/client/src/app/dashboard/login/page.tsx new file mode 100644 index 0000000..e912f81 --- /dev/null +++ b/client/src/app/dashboard/login/page.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import Layout from '@/components/Layout'; +import { FormEvent, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +export default function Login() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const [messageType, setMessageType] = useState<"success" | "error" | "">(""); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setLoading(true); + + const response = await fetch( + `${process.env.NEXT_PUBLIC_USER_DATABASE}/login`, + { + method: "POST", + credentials: "include", // Send/receive cookies + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + } + ); + + if (response.ok) { + const responseData = await response.json(); + + // Assuming the token is returned in the responseData + const token = responseData.token; + + // Decode the token's payload to check for user role + const payload = JSON.parse(atob(token.split(".")[1])); + const userRole = payload.role; + + setMessage(`Successful login for Email: ${email}`); + setMessageType("success"); + + // Redirect based on user role + if (userRole === "admin") { + router.push("/admin"); + } else { + router.push("/user"); + } + } else { + const errorData = await response.json(); + setMessage(`Login failed: ${errorData.message}`); + setMessageType("error"); + } + + setLoading(false); + }; + + return ( + +
+

Login Page!

+
+
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + + + {loading &&

Loading...

} + {message && ( +

+ {message} +

+ )} + +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/app/dashboard/resetPassword/page.tsx b/client/src/app/dashboard/resetPassword/page.tsx new file mode 100644 index 0000000..6d11e33 --- /dev/null +++ b/client/src/app/dashboard/resetPassword/page.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import Layout from '@/components/Layout'; +import { FormEvent, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useSearchParams } from "next/navigation"; + +export default function ResetPassword() { + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [message, setMessage] = useState(""); + const [messageType, setMessageType] = useState<"success" | "error" | "">(""); + const [loading, setLoading] = useState(false); + const searchParams = useSearchParams(); + const router = useRouter(); + + const resetToken = searchParams.get("token"); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setLoading(true); + + if (password !== confirmPassword) { + setMessage("Passwords do not match."); + setMessageType("error"); + setLoading(false); + return; + } + + const response = await fetch( + `${process.env.NEXT_PUBLIC_USER_DATABASE}/resetPassword`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token: resetToken, password }), + } + ); + + if (response.ok) { + setMessage("Password successfully reset."); + setMessageType("success"); + router.push("/dashboard/login"); + } else { + const errorData = await response.json(); + setMessage(`Error: ${errorData.message}`); + setMessageType("error"); + } + + setLoading(false); + }; + + return ( + +
+

Reset Password

+
+
+ setPassword(e.target.value)} + /> + setConfirmPassword(e.target.value)} + /> + + + {message && ( +

+ {message} +

+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx index 7d0cff2..a600a4f 100644 --- a/client/src/app/layout.tsx +++ b/client/src/app/layout.tsx @@ -4,10 +4,12 @@ import "./globals.css"; import Header from '@/components/ui/Header'; import Footer from '@/components/ui/Footer'; +/* export const metadata: Metadata = { title: "Inside Out", description: "Generated by create next app", }; +*/ export default function RootLayout({ children, @@ -25,4 +27,4 @@ export default function RootLayout({ ); -} +} \ No newline at end of file diff --git a/client/src/app/login/forgotPassword/page.tsx b/client/src/app/login/forgotPassword/page.tsx new file mode 100644 index 0000000..5560560 --- /dev/null +++ b/client/src/app/login/forgotPassword/page.tsx @@ -0,0 +1,81 @@ +//src/app/login/forgotPassword/page.tsx +// Forgot Password Page + +"use client"; + +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import Layout from '@/app/layout'; +import Link from "next/link"; +import { FormEvent, useState } from "react"; + +export default function ForgotPassword() { + const [email, setEmail] = useState(""); + const [message, setMessage] = useState(""); + const [messageType, setMessageType] = useState<"success" | "error" | "">(""); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setLoading(true); + + // Call API to initiate the password reset + const response = await fetch( + `${process.env.NEXT_PUBLIC_USER_DATABASE}/forgotPassword`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + } + ); + + if (response.ok) { + setMessage("Password reset link sent to your email."); + setMessageType("success"); + } else { + const errorData = await response.json(); + setMessage(`Error: ${errorData.message}`); + setMessageType("error"); + } + + setLoading(false); + }; + + return ( + + +
+

Forgot Password

+
+
+ setEmail(e.target.value)} + /> + + + {message && ( +

+ {message} +

+ )} + +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/app/login/page.tsx b/client/src/app/login/page.tsx index fb3cafd..ea78917 100644 --- a/client/src/app/login/page.tsx +++ b/client/src/app/login/page.tsx @@ -79,11 +79,14 @@ export default function Login() { {message}

)} - + ); diff --git a/client/src/app/login/resetPassword/page.tsx b/client/src/app/login/resetPassword/page.tsx new file mode 100644 index 0000000..181a789 --- /dev/null +++ b/client/src/app/login/resetPassword/page.tsx @@ -0,0 +1,94 @@ +//src/app/login/resetPassword/page.tsx +// Reset Password Page + +"use client"; + +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import Layout from '@/app/layout'; +import { FormEvent, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useSearchParams } from "next/navigation"; + +export default function ResetPassword() { + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [message, setMessage] = useState(""); + const [messageType, setMessageType] = useState<"success" | "error" | "">(""); + const [loading, setLoading] = useState(false); + const searchParams = useSearchParams(); + const router = useRouter(); + + const resetToken = searchParams.get("token"); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setLoading(true); + + if (password !== confirmPassword) { + setMessage("Passwords do not match."); + setMessageType("error"); + setLoading(false); + return; + } + + const response = await fetch( + `${process.env.NEXT_PUBLIC_USER_DATABASE}/resetPassword`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token: resetToken, password }), + } + ); + + if (response.ok) { + setMessage("Password successfully reset."); + setMessageType("success"); + router.push("/dashboard/login"); + } else { + const errorData = await response.json(); + setMessage(`Error: ${errorData.message}`); + setMessageType("error"); + } + + setLoading(false); + }; + + return ( + +
+

Reset Password

+
+
+ setPassword(e.target.value)} + /> + setConfirmPassword(e.target.value)} + /> + + + {message && ( +

+ {message} +

+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/databaseServer/classes/forgotPasswordHandler.js b/databaseServer/classes/forgotPasswordHandler.js new file mode 100644 index 0000000..bb61910 --- /dev/null +++ b/databaseServer/classes/forgotPasswordHandler.js @@ -0,0 +1,49 @@ +import crypto from 'crypto'; +import nodemailer from 'nodemailer'; + +export async function forgotPasswordHandler(email, supabase) { + try { + // Check if the user exists + const { data: user, error } = await supabase + .from('users') + .select('*') + .eq('email', email) + .single(); + + if (error || !user) { + throw new Error('User not found'); + } + + // Generate a reset token and expiration time + const resetToken = crypto.randomBytes(32).toString('hex'); + const expiration = Date.now() + 3600000; // Token valid for 1 hour + + // Save the token and expiration in the user's record + await supabase + .from('users') + .update({ reset_token: resetToken, reset_token_expiration: expiration }) + .eq('email', email); + + // Set up Nodemailer transporter + const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL_USER, // Store credentials in environment variables + pass: process.env.EMAIL_PASS, + }, + }); + + // Send password reset email with the token link + const resetLink = `https://localhost:3000/dashboard/resetPassword?token=${resetToken}`; + await transporter.sendMail({ + to: email, + subject: 'Password Reset Request', + html: `

Click here to reset your password. This link will expire in 1 hour.

`, + }); + + return { message: 'Password reset link sent' }; + } catch (error) { + console.error('Error in forgotPasswordHandler:', error); + throw new Error(error.message); + } +} \ No newline at end of file diff --git a/databaseServer/classes/resetPasswordHandler.js b/databaseServer/classes/resetPasswordHandler.js new file mode 100644 index 0000000..11ad347 --- /dev/null +++ b/databaseServer/classes/resetPasswordHandler.js @@ -0,0 +1,37 @@ +import bcrypt from 'bcrypt'; + +const saltRounds = 10; + +export async function resetPasswordHandler(token, newPassword, supabase) { + try { + // Find the user with the provided token + const { data: user, error } = await supabase + .from('users') + .select('*') + .eq('reset_token', token) + .single(); + + if (error || !user) { + throw new Error('Invalid or expired token'); + } + + // Check if the token is expired + if (Date.now() > user.reset_token_expiration) { + throw new Error('Token has expired'); + } + + // Hash the new password + const hashedPassword = await bcrypt.hash(newPassword, saltRounds); + + // Update the user's password and clear the reset token and expiration + await supabase + .from('users') + .update({ password: hashedPassword, reset_token: null, reset_token_expiration: null }) + .eq('id', user.id); + + return { message: 'Password has been reset successfully' }; + } catch (error) { + console.error('Error in resetPasswordHandler:', error); + throw new Error(error.message); + } +} \ No newline at end of file diff --git a/lightServer b/lightServer new file mode 160000 index 0000000..7d8f3ba --- /dev/null +++ b/lightServer @@ -0,0 +1 @@ +Subproject commit 7d8f3ba6ea7379e4dea12784d06109932143bdf2
+ {messages.table.noUserStats} -