diff --git a/backend/imk-backend/.env b/backend/imk-backend/.env index fed6ef1..15ce900 100644 --- a/backend/imk-backend/.env +++ b/backend/imk-backend/.env @@ -6,6 +6,7 @@ DATABASE_URL="postgresql://root:admin@localhost:5432/imk?schema=public" JWT_SECRET=some-secret +JWT_RESET_SECRET=some-reset-secret-key AWS_REGION=EU2 AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6 AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4 diff --git a/backend/imk-backend/src/auth/auth.controller.ts b/backend/imk-backend/src/auth/auth.controller.ts index 5662e0b..15fee33 100644 --- a/backend/imk-backend/src/auth/auth.controller.ts +++ b/backend/imk-backend/src/auth/auth.controller.ts @@ -6,6 +6,7 @@ import { UseGuards, Get, Request, + BadRequestException, } from '@nestjs/common'; import { AuthService } from './auth.service'; import { LoginDto } from '../dto/login.dto'; @@ -39,6 +40,23 @@ export class AuthController { async createAdmin(@Body() createUserDto: CreateUserDto) { return this.authService.createUser(createUserDto); } + @Post('forgot-password') + async forgotPassword(@Body('email') email: string) { + if (!email) { + throw new BadRequestException('Email is required'); + } + return this.authService.requestPasswordReset(email); + } + + @Post('reset-password') + async resetPassword( + @Body() body: { token: string; newPassword: string }, + ) { + if (!body.token || !body.newPassword) { + throw new BadRequestException('Token and new password are required'); + } + return this.authService.resetPassword(body.token, body.newPassword); + } @UseGuards(JwtAuthGuard) @Get('user-info') diff --git a/backend/imk-backend/src/auth/auth.service.ts b/backend/imk-backend/src/auth/auth.service.ts index 8b55e40..700d745 100644 --- a/backend/imk-backend/src/auth/auth.service.ts +++ b/backend/imk-backend/src/auth/auth.service.ts @@ -1,4 +1,4 @@ -import { Injectable, ConflictException } from '@nestjs/common'; +import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { PrismaService } from '../prisma/prisma.service'; import * as bcrypt from 'bcrypt'; @@ -41,35 +41,6 @@ export class AuthService { }; } - // async createUser( - // createUserDto: CreateUserDto, - // isAdmin: boolean = false, - // ): Promise { - // const existingUser = await this.prisma.user.findUnique({ - // where: { email: createUserDto.email }, - // }); - - // if (existingUser) { - // throw new ConflictException('Email already exists'); - // } - - // const hashedPassword = await bcrypt.hash(createUserDto.password, 10); - - // const newUser = await this.prisma.user.create({ - // data: { - // email: createUserDto.email, - // password: hashedPassword, - // name: createUserDto.name, - // isAdmin: isAdmin, - // }, - // }); - - // // eslint-disable-next-line @typescript-eslint/no-unused-vars - // const { password, ...result } = newUser; - // console.log(result); - // return result; - // } - async createUser(createUserDto: CreateUserDto) { const user = await this.prisma.user.create({ data: { @@ -85,14 +56,35 @@ export class AuthService { async requestPasswordReset(email: string) { const user = await this.prisma.user.findUnique({ where: { email } }); - if (!user) return; + if (!user) { + throw new NotFoundException('User not found'); + } const resetToken = this.jwtService.sign( - { email }, + { email, userId: user.id }, { expiresIn: '1h', secret: process.env.JWT_RESET_SECRET }, ); await this.emailService.sendPasswordResetEmail(email, resetToken); + return { message: 'Password reset instructions sent to your email' }; + } + + async resetPassword(token: string, newPassword: string) { + try { + const payload = this.jwtService.verify(token, { + secret: process.env.JWT_RESET_SECRET, + }); + + const hashedPassword = await bcrypt.hash(newPassword, 10); + await this.prisma.user.update({ + where: { email: payload.email }, + data: { password: hashedPassword }, + }); + + return { message: 'Password reset successful' }; + } catch (error) { + throw new BadRequestException('Invalid or expired reset token'); + } } async getUserInfo(userId: number) { if (!userId) { diff --git a/frontend/imk/src/App.jsx b/frontend/imk/src/App.jsx index 39c4a86..255915b 100644 --- a/frontend/imk/src/App.jsx +++ b/frontend/imk/src/App.jsx @@ -16,6 +16,8 @@ import AdminPanel from './components/adminPanel/AdminPanel'; import Dashboard from './components/dashboard/Dashboard'; import Login from './components/login/login'; import ProtectedRoute from './components/protectedRoute/ProtectedRoute'; +import ForgotPassword from './components/ForgotPassword'; +import ResetPassword from './components/ResetPassword'; function App() { @@ -33,6 +35,8 @@ function App() { } /> } /> } /> + } /> + } /> diff --git a/frontend/imk/src/components/ForgotPassword.jsx b/frontend/imk/src/components/ForgotPassword.jsx new file mode 100644 index 0000000..5af4ff8 --- /dev/null +++ b/frontend/imk/src/components/ForgotPassword.jsx @@ -0,0 +1,69 @@ +import { useState } from 'react'; +import axios from 'axios'; + +export default function ForgotPassword() { + const [email, setEmail] = useState(''); + const [status, setStatus] = useState({ type: '', message: '' }); + + const handleSubmit = async (e) => { + e.preventDefault(); + setStatus({ type: 'loading', message: 'Sending reset instructions...' }); + + try { + await axios.post(`http://localhost:3000/auth/forgot-password`, { email }); + setStatus({ + type: 'success', + message: 'Password reset instructions have been sent to your email.', + }); + } catch (error) { + setStatus({ + type: 'error', + message: error.response?.data?.message || 'An error occurred. Please try again.', + }); + } + }; + + return ( +
+
+

+ Reset your password +

+
+ +
+
+
+
+ + setEmail(e.target.value)} + className="mt-1 block w-full rounded-md border-gray-300 bg-gray-700 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500" + /> +
+ + {status.message && ( +
+ {status.message} +
+ )} + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/imk/src/components/ResetPassword.jsx b/frontend/imk/src/components/ResetPassword.jsx new file mode 100644 index 0000000..67439af --- /dev/null +++ b/frontend/imk/src/components/ResetPassword.jsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import axios from 'axios'; + +export default function ResetPassword() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [status, setStatus] = useState({ type: '', message: '' }); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (password !== confirmPassword) { + setStatus({ type: 'error', message: 'Passwords do not match' }); + return; + } + + setStatus({ type: 'loading', message: 'Resetting password...' }); + const token = searchParams.get('token'); + + try { + await axios.post(`http://localhost:3000/auth/reset-password`, { + token, + newPassword: password, + }); + setStatus({ type: 'success', message: 'Password reset successful!' }); + setTimeout(() => navigate('/login'), 2000); + } catch (error) { + setStatus({ + type: 'error', + message: error.response?.data?.message || 'An error occurred. Please try again.', + }); + } + }; + + return ( +
+
+

+ Reset your password +

+
+ +
+
+
+
+ + setPassword(e.target.value)} + className="mt-1 block w-full rounded-md border-gray-300 bg-gray-700 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="mt-1 block w-full rounded-md border-gray-300 bg-gray-700 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500" + /> +
+ + {status.message && ( +
+ {status.message} +
+ )} + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/imk/src/components/dashboard/Dashboard.jsx b/frontend/imk/src/components/dashboard/Dashboard.jsx index 515268b..6598a9b 100644 --- a/frontend/imk/src/components/dashboard/Dashboard.jsx +++ b/frontend/imk/src/components/dashboard/Dashboard.jsx @@ -1,8 +1,9 @@ import { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; import { useAuth } from '../../hooks/useAuth'; import { getSharedDocuments } from '../../services/api'; import { format } from 'date-fns'; -import { FiFolder, FiFile, FiDownload, FiChevronRight, FiLoader } from 'react-icons/fi'; +import { FiFolder, FiFile, FiDownload, FiChevronRight, FiLoader, FiUser, FiKey, FiMail } from 'react-icons/fi'; import { downloadDocument } from '../../services/api'; function Dashboard() { @@ -11,6 +12,13 @@ function Dashboard() { const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const { user } = useAuth(); + const [userInfo, setUserInfo] = useState(null); + + useEffect(() => { + if (user) { + setUserInfo(user); + } + }, [user]); useEffect(() => { const fetchDocuments = async () => { @@ -88,10 +96,47 @@ function Dashboard() { return (
-
+ {/*

Your Documents

Access and manage your shared documents

-
+
*/} + {/* User Info Card */} +
+
+
+ +
+
+

Welcome, {userInfo?.name}

+

{userInfo?.email}

+
+
+
+
+ + +
+

Reset Password

+

Change your current password

+
+ + + + +
+

Forgot Password

+

Request a password reset link

+
+ +
+ +
{Object.entries(documents).length > 0 ? ( @@ -144,7 +189,8 @@ function Dashboard() {

No documents available

- )} + )} +