From 66f7c297a1a4afff95e7011c00c0435705b8cc0a Mon Sep 17 00:00:00 2001
From: RanP90 <122150955+RanP90@users.noreply.github.com>
Date: Sat, 2 Nov 2024 01:29:25 -0700
Subject: [PATCH 1/4] Feature: Initial implement of forgotPassword
---
.../src/app/dashboard/forgotPassword/page.tsx | 78 ++++++++++++++++
client/src/app/dashboard/login/page.tsx | 3 +
.../src/app/dashboard/resetPassword/page.tsx | 91 +++++++++++++++++++
3 files changed, 172 insertions(+)
create mode 100644 client/src/app/dashboard/forgotPassword/page.tsx
create mode 100644 client/src/app/dashboard/resetPassword/page.tsx
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
+
+
+ {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
index 5976a54..e912f81 100644
--- a/client/src/app/dashboard/login/page.tsx
+++ b/client/src/app/dashboard/login/page.tsx
@@ -95,6 +95,9 @@ export default function Login() {
Back
+
+ Forgot Password?
+
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
+
+
+ {message && (
+
+ {message}
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
From 970bdc8aceda92515ede7174415cca506fb619f9 Mon Sep 17 00:00:00 2001
From: RanP90 <122150955+RanP90@users.noreply.github.com>
Date: Sat, 2 Nov 2024 01:51:18 -0700
Subject: [PATCH 2/4] Feature: Initial implement of forgot/reset password
handler
---
.../classes/forgotPasswordHandler.js | 49 +++++++++++++++++++
.../classes/resetPasswordHandler.js | 37 ++++++++++++++
2 files changed, 86 insertions(+)
create mode 100644 databaseServer/classes/forgotPasswordHandler.js
create mode 100644 databaseServer/classes/resetPasswordHandler.js
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
From f409131faf6e31504205c055897b205ed1ba1f08 Mon Sep 17 00:00:00 2001
From: RanP90 <122150955+RanP90@users.noreply.github.com>
Date: Thu, 21 Nov 2024 00:07:26 -0800
Subject: [PATCH 3/4] Fix: Update API endpoint
---
.DS_Store | Bin 0 -> 6148 bytes
client/src/app/admin/page.tsx | 185 ++++++++++---------
client/src/app/layout.tsx | 4 +-
client/src/app/login/forgotPassword/page.tsx | 81 ++++++++
client/src/app/login/page.tsx | 13 +-
client/src/app/login/resetPassword/page.tsx | 94 ++++++++++
lightServer | 1 +
7 files changed, 288 insertions(+), 90 deletions(-)
create mode 100644 .DS_Store
create mode 100644 client/src/app/login/forgotPassword/page.tsx
create mode 100644 client/src/app/login/resetPassword/page.tsx
create mode 160000 lightServer
diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..ada1d2667efb1fcd9856b85ee382bf04d2840bf8
GIT binary patch
literal 6148
zcmeHK%}T>S5T4NrfnIv_xMy#DgIMAd*ceSPy+0`vxoH*KlzL
zoB?OR8E^(>7_e8OSkCy}> {
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; token?: string }) => ({
+ (user: { id: string; email: string; username?: string }) => ({
+ username: user.username || `User ID: ${user.id}`,
+ email: user.email,
+ //token: user.token || "N/A",
+ 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 +145,10 @@ const AdminDashboard: React.FC = () => {
{error}
) : (
<>
- {messages.dashboard.apiStatsTitle}
+ {/* API Stats Table */}
+
+ {messages.dashboard.apiStatsTitle}
+
@@ -156,20 +168,24 @@ const AdminDashboard: React.FC = () => {
))
) : (
- |
+
{messages.table.noApiStats}
- |
+
)}
- {messages.dashboard.userStatsTitle}
+ {/* User Stats Table */}
+
+ {messages.dashboard.userStatsTitle}
+
+ Username
Email
- Token
+ {/* Token */}
Total Requests
@@ -177,16 +193,17 @@ const AdminDashboard: React.FC = () => {
{userStats.length ? (
userStats.map((user, index) => (
+ {user.username}
{user.email}
- {user.token}
+ {/* {user.token}*/}
{user.totalRequests}
))
) : (
- |
+
{messages.table.noUserStats}
- |
+
)}
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({