Compare commits
No commits in common. "8a4ecd913cbe67f6dec67467998227be9bcc8757" and "0bdce7973943ef886b7ff6a0ad5312a158cf63a3" have entirely different histories.
8a4ecd913c
...
0bdce79739
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "PasswordReset" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"token" TEXT NOT NULL,
|
|
||||||
"userId" INTEGER NOT NULL,
|
|
||||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
"used" BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "PasswordReset_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "PasswordReset_token_key" ON "PasswordReset"("token");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "PasswordReset" ADD CONSTRAINT "PasswordReset_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
@ -29,9 +29,6 @@ model User {
|
|||||||
uploadedDocuments Document[] @relation("UploadedDocuments")
|
uploadedDocuments Document[] @relation("UploadedDocuments")
|
||||||
Notification Notification[]
|
Notification Notification[]
|
||||||
sharedDocuments Document[] @relation("SharedDocuments")
|
sharedDocuments Document[] @relation("SharedDocuments")
|
||||||
passwordResets PasswordReset[]
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Notification {
|
model Notification {
|
||||||
@ -44,13 +41,3 @@ model Notification {
|
|||||||
document Document @relation(fields: [documentId], references: [id])
|
document Document @relation(fields: [documentId], references: [id])
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
model PasswordReset {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
token String @unique
|
|
||||||
userId Int
|
|
||||||
user User @relation(fields: [userId], references: [id])
|
|
||||||
expiresAt DateTime
|
|
||||||
used Boolean @default(false)
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
@ -102,16 +103,6 @@ async uploadDocument(
|
|||||||
return this.adminService.updateDocumentStatus(+id, status);
|
return this.adminService.updateDocumentStatus(+id, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('users/:id/reset-password')
|
|
||||||
async resetUserPassword(
|
|
||||||
@Param('id', ParseIntPipe) id: number,
|
|
||||||
@Body() { password }: { password: string },
|
|
||||||
) {
|
|
||||||
if (!password || password.length < 6) {
|
|
||||||
throw new BadRequestException('Password must be at least 6 characters long');
|
|
||||||
}
|
|
||||||
return this.adminService.resetUserPassword(id, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Get('test-s3-connection')
|
// @Get('test-s3-connection')
|
||||||
// async testS3Connection() {
|
// async testS3Connection() {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { S3Service } from '../s3/s3.service';
|
import { S3Service } from '../s3/s3.service';
|
||||||
import { UpdateDocumentDto } from '../dto/update-document.dto';
|
import { UpdateDocumentDto } from '../dto/update-document.dto';
|
||||||
@ -339,65 +339,4 @@ export class AdminService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetUserPassword(userId: number, newPassword: string) {
|
|
||||||
this.logger.log('=== Starting password reset process ===');
|
|
||||||
this.logger.debug('Reset request for user:', { userId });
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find the user
|
|
||||||
const user = await this.prisma.user.findUnique({
|
|
||||||
where: { id: userId },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
this.logger.error('User not found:', { userId });
|
|
||||||
throw new NotFoundException('User not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash the new password
|
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
||||||
|
|
||||||
// Update the user's password
|
|
||||||
await this.prisma.user.update({
|
|
||||||
where: { id: userId },
|
|
||||||
data: { password: hashedPassword },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log('Password reset successful for user:', {
|
|
||||||
userId: user.id,
|
|
||||||
email: user.email,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send email notification
|
|
||||||
try {
|
|
||||||
await this.emailService.sendPasswordResetNotification(
|
|
||||||
user.email,
|
|
||||||
user.name
|
|
||||||
);
|
|
||||||
this.logger.debug('Password reset notification sent');
|
|
||||||
} catch (emailError) {
|
|
||||||
this.logger.error('Failed to send password reset notification:', {
|
|
||||||
error: emailError.message,
|
|
||||||
code: emailError.code,
|
|
||||||
stack: emailError.stack,
|
|
||||||
});
|
|
||||||
// Don't throw the error, just log it
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: 'Password reset successful' };
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Error in resetUserPassword:', {
|
|
||||||
error: error.message,
|
|
||||||
code: error.code,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -7,7 +7,6 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
Request,
|
Request,
|
||||||
Logger,
|
Logger,
|
||||||
BadRequestException,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { LoginDto } from '../dto/login.dto';
|
import { LoginDto } from '../dto/login.dto';
|
||||||
@ -155,25 +154,4 @@ export class AuthController {
|
|||||||
async getUserInfo(@Request() req) {
|
async getUserInfo(@Request() req) {
|
||||||
return this.authService.getUserInfo(req.user.userId);
|
return this.authService.getUserInfo(req.user.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('forgot-password')
|
|
||||||
async forgotPassword(@Body() { email }: { email: string }) {
|
|
||||||
if (!email) {
|
|
||||||
throw new BadRequestException('Email is required');
|
|
||||||
}
|
|
||||||
return this.authService.sendPasswordResetToken(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('reset-password')
|
|
||||||
async resetPassword(
|
|
||||||
@Body() { token, newPassword }: { token: string; newPassword: string }
|
|
||||||
) {
|
|
||||||
if (!token || !newPassword) {
|
|
||||||
throw new BadRequestException('Token and new password are required');
|
|
||||||
}
|
|
||||||
if (newPassword.length < 6) {
|
|
||||||
throw new BadRequestException('Password must be at least 6 characters long');
|
|
||||||
}
|
|
||||||
return this.authService.resetPasswordWithToken(token, newPassword);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import { Injectable, ConflictException, Logger, UnauthorizedException, NotFoundException } from '@nestjs/common';
|
import { Injectable, ConflictException, Logger } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { CreateUserDto } from '../dto/create-user.dto';
|
import { CreateUserDto } from '../dto/create-user.dto';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { EmailService } from '../email/email.service';
|
import { EmailService } from '../email/email.service';
|
||||||
import { randomBytes } from 'crypto';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@ -227,135 +226,4 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendPasswordResetToken(email: string) {
|
|
||||||
this.logger.log('=== Starting password reset token process ===');
|
|
||||||
this.logger.debug('Reset token requested for:', email);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find the user
|
|
||||||
const user = await this.prisma.user.findUnique({
|
|
||||||
where: { email },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
this.logger.warn('User not found for password reset:', email);
|
|
||||||
// Return success anyway to prevent email enumeration
|
|
||||||
return { message: 'If an account exists, a password reset link has been sent' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate reset token
|
|
||||||
const token = randomBytes(32).toString('hex');
|
|
||||||
const expiresAt = new Date();
|
|
||||||
expiresAt.setHours(expiresAt.getHours() + 1); // Token expires in 1 hour
|
|
||||||
|
|
||||||
// Save reset token
|
|
||||||
await this.prisma.passwordReset.create({
|
|
||||||
data: {
|
|
||||||
token,
|
|
||||||
userId: user.id,
|
|
||||||
expiresAt,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send email with reset link
|
|
||||||
try {
|
|
||||||
await this.emailService.sendPasswordResetEmail(
|
|
||||||
user.email,
|
|
||||||
user.name,
|
|
||||||
token
|
|
||||||
);
|
|
||||||
this.logger.debug('Password reset email sent successfully');
|
|
||||||
} catch (emailError) {
|
|
||||||
this.logger.error('Failed to send password reset email:', {
|
|
||||||
error: emailError.message,
|
|
||||||
code: emailError.code,
|
|
||||||
stack: emailError.stack,
|
|
||||||
});
|
|
||||||
throw emailError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: 'If an account exists, a password reset link has been sent' };
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Error in sendPasswordResetToken:', {
|
|
||||||
error: error.message,
|
|
||||||
code: error.code,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async resetPasswordWithToken(token: string, newPassword: string) {
|
|
||||||
this.logger.log('=== Starting password reset with token process ===');
|
|
||||||
this.logger.debug('Password reset attempted with token');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find valid reset token
|
|
||||||
const resetRecord = await this.prisma.passwordReset.findFirst({
|
|
||||||
where: {
|
|
||||||
token,
|
|
||||||
expiresAt: {
|
|
||||||
gt: new Date(), // Token must not be expired
|
|
||||||
},
|
|
||||||
used: false, // Token must not be used
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
user: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resetRecord) {
|
|
||||||
this.logger.warn('Invalid or expired reset token used');
|
|
||||||
throw new UnauthorizedException('Invalid or expired reset token');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash new password
|
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
||||||
|
|
||||||
// Update password and mark token as used
|
|
||||||
await this.prisma.$transaction([
|
|
||||||
this.prisma.user.update({
|
|
||||||
where: { id: resetRecord.userId },
|
|
||||||
data: { password: hashedPassword },
|
|
||||||
}),
|
|
||||||
this.prisma.passwordReset.update({
|
|
||||||
where: { id: resetRecord.id },
|
|
||||||
data: { used: true },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this.logger.log('Password reset successful for user:', resetRecord.userId);
|
|
||||||
|
|
||||||
// Send confirmation email
|
|
||||||
try {
|
|
||||||
await this.emailService.sendPasswordChangeConfirmation(
|
|
||||||
resetRecord.user.email,
|
|
||||||
resetRecord.user.name
|
|
||||||
);
|
|
||||||
this.logger.debug('Password change confirmation email sent');
|
|
||||||
} catch (emailError) {
|
|
||||||
this.logger.error('Failed to send password change confirmation:', {
|
|
||||||
error: emailError.message,
|
|
||||||
code: emailError.code,
|
|
||||||
stack: emailError.stack,
|
|
||||||
});
|
|
||||||
// Don't throw error here as password is already changed
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: 'Password reset successful' };
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Error in resetPasswordWithToken:', {
|
|
||||||
error: error.message,
|
|
||||||
code: error.code,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -186,118 +186,4 @@ export class EmailService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendPasswordResetNotification(email: string, name: string) {
|
|
||||||
this.logger.log('Sending password reset notification email to:', email);
|
|
||||||
|
|
||||||
const mailOptions = {
|
|
||||||
from: this.from,
|
|
||||||
to: email,
|
|
||||||
subject: 'Your Password Has Been Reset',
|
|
||||||
html: `
|
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
||||||
<h2>Password Reset Notification</h2>
|
|
||||||
<p>Hello ${name},</p>
|
|
||||||
<p>Your password has been reset by an administrator.</p>
|
|
||||||
<p>If you did not expect this change, please contact your administrator immediately.</p>
|
|
||||||
<p>Best regards,<br>IMK Team</p>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.logger.debug('Attempting to send password reset notification...');
|
|
||||||
const info = await this.transporter.sendMail(mailOptions);
|
|
||||||
this.logger.log('Password reset notification sent successfully:', info.response);
|
|
||||||
return info;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to send password reset notification:', {
|
|
||||||
error: error.message,
|
|
||||||
code: error.code,
|
|
||||||
command: error.command,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendPasswordResetEmail(email: string, name: string, token: string) {
|
|
||||||
this.logger.log('Sending password reset email to:', email);
|
|
||||||
|
|
||||||
const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${token}`;
|
|
||||||
const mailOptions = {
|
|
||||||
from: this.from,
|
|
||||||
to: email,
|
|
||||||
subject: 'Reset Your Password - IMK Platform',
|
|
||||||
html: `
|
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
||||||
<h2>Password Reset Request</h2>
|
|
||||||
<p>Hello ${name},</p>
|
|
||||||
<p>We received a request to reset your password. Click the link below to set a new password:</p>
|
|
||||||
<p>
|
|
||||||
<a href="${resetLink}" style="
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background-color: #4F46E5;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
">Reset Password</a>
|
|
||||||
</p>
|
|
||||||
<p>This link will expire in 1 hour.</p>
|
|
||||||
<p>If you didn't request this, please ignore this email or contact support if you have concerns.</p>
|
|
||||||
<p>Best regards,<br>IMK Team</p>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.logger.debug('Attempting to send password reset email...');
|
|
||||||
const info = await this.transporter.sendMail(mailOptions);
|
|
||||||
this.logger.log('Password reset email sent successfully:', info.response);
|
|
||||||
return info;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to send password reset email:', {
|
|
||||||
error: error.message,
|
|
||||||
code: error.code,
|
|
||||||
command: error.command,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendPasswordChangeConfirmation(email: string, name: string) {
|
|
||||||
this.logger.log('Sending password change confirmation to:', email);
|
|
||||||
|
|
||||||
const mailOptions = {
|
|
||||||
from: this.from,
|
|
||||||
to: email,
|
|
||||||
subject: 'Password Changed Successfully - IMK Platform',
|
|
||||||
html: `
|
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
||||||
<h2>Password Changed Successfully</h2>
|
|
||||||
<p>Hello ${name},</p>
|
|
||||||
<p>Your password has been successfully changed.</p>
|
|
||||||
<p>If you did not make this change, please contact our support team immediately.</p>
|
|
||||||
<p>Best regards,<br>IMK Team</p>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.logger.debug('Attempting to send password change confirmation...');
|
|
||||||
const info = await this.transporter.sendMail(mailOptions);
|
|
||||||
this.logger.log('Password change confirmation sent successfully:', info.response);
|
|
||||||
return info;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to send password change confirmation:', {
|
|
||||||
error: error.message,
|
|
||||||
code: error.code,
|
|
||||||
command: error.command,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -17,8 +17,6 @@ import Login from './components/login/Login';
|
|||||||
import Navbar from './components/navbar/Navbar';
|
import Navbar from './components/navbar/Navbar';
|
||||||
import ProtectedRoute from './components/protectedRoute/ProtectedRoute';
|
import ProtectedRoute from './components/protectedRoute/ProtectedRoute';
|
||||||
import { ThemeProvider } from './theme/ThemeProvider';
|
import { ThemeProvider } from './theme/ThemeProvider';
|
||||||
import ForgotPassword from './components/auth/ForgotPassword';
|
|
||||||
import ResetPassword from './components/auth/ResetPassword';
|
|
||||||
// import { Navbar } from './components/navbar/Navbar';
|
// import { Navbar } from './components/navbar/Navbar';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@ -54,8 +52,6 @@ function App() {
|
|||||||
<Dashboard />
|
<Dashboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path='/forgot-password' element={<ForgotPassword />} />
|
|
||||||
<Route path='/reset-password' element={<ResetPassword />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
<Footer />
|
<Footer />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { getAllUsers, getAllDocuments, getUserInfo, createUser, resetUserPassword } from '../../services/api';
|
import { getAllUsers, getAllDocuments, getUserInfo, createUser } from '../../services/api';
|
||||||
import DocumentUpload from '../documentUpload/DocumentUpload';
|
import DocumentUpload from '../documentUpload/DocumentUpload';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { FiUsers, FiFile, FiUpload, FiUserPlus, FiLoader, FiKey } from 'react-icons/fi';
|
import { FiUsers, FiFile, FiUpload, FiUserPlus, FiLoader } from 'react-icons/fi';
|
||||||
|
|
||||||
function AdminPanel() {
|
function AdminPanel() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -12,12 +12,6 @@ function AdminPanel() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [resetPasswordModal, setResetPasswordModal] = useState({
|
|
||||||
isOpen: false,
|
|
||||||
userId: null,
|
|
||||||
userName: '',
|
|
||||||
newPassword: '',
|
|
||||||
});
|
|
||||||
const [newUser, setNewUser] = useState({
|
const [newUser, setNewUser] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
@ -86,24 +80,6 @@ function AdminPanel() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetPassword = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
await resetUserPassword(resetPasswordModal.userId, resetPasswordModal.newPassword);
|
|
||||||
setResetPasswordModal({
|
|
||||||
isOpen: false,
|
|
||||||
userId: null,
|
|
||||||
userName: '',
|
|
||||||
newPassword: '',
|
|
||||||
});
|
|
||||||
// Show success message
|
|
||||||
setError('Password reset successful');
|
|
||||||
setTimeout(() => setError(''), 3000);
|
|
||||||
} catch (err) {
|
|
||||||
setError('Failed to reset password');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isAdmin) return null;
|
if (!isAdmin) return null;
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
@ -156,55 +132,6 @@ function AdminPanel() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{resetPasswordModal.isOpen && (
|
|
||||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
|
|
||||||
<div className="bg-primary-800 rounded-xl p-6 w-full max-w-md">
|
|
||||||
<h3 className="text-xl font-bold text-white mb-4">
|
|
||||||
Reset Password for {resetPasswordModal.userName}
|
|
||||||
</h3>
|
|
||||||
<form onSubmit={handleResetPassword} className="space-y-4">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="New Password"
|
|
||||||
value={resetPasswordModal.newPassword}
|
|
||||||
onChange={(e) => setResetPasswordModal({
|
|
||||||
...resetPasswordModal,
|
|
||||||
newPassword: e.target.value
|
|
||||||
})}
|
|
||||||
className="w-full bg-primary-700/30 border border-primary-600 rounded-lg px-4 py-2
|
|
||||||
text-white placeholder-neutral-400 focus:outline-none focus:border-primary-500
|
|
||||||
focus:ring-1 focus:ring-primary-500"
|
|
||||||
required
|
|
||||||
minLength={6}
|
|
||||||
/>
|
|
||||||
<div className="flex justify-end space-x-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setResetPasswordModal({
|
|
||||||
isOpen: false,
|
|
||||||
userId: null,
|
|
||||||
userName: '',
|
|
||||||
newPassword: '',
|
|
||||||
})}
|
|
||||||
className="px-4 py-2 text-neutral-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex items-center justify-center space-x-2 px-4 py-2
|
|
||||||
bg-primary-600 hover:bg-primary-700 text-white rounded-lg
|
|
||||||
transition-colors shadow-lg"
|
|
||||||
>
|
|
||||||
<FiKey className="w-4 h-4" />
|
|
||||||
<span>Reset Password</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
{activeTab === 'documents' && (
|
{activeTab === 'documents' && (
|
||||||
<div className="bg-primary-800/50 backdrop-blur-lg rounded-xl overflow-hidden shadow-xl">
|
<div className="bg-primary-800/50 backdrop-blur-lg rounded-xl overflow-hidden shadow-xl">
|
||||||
@ -321,7 +248,6 @@ function AdminPanel() {
|
|||||||
<th className="px-6 py-4 text-left text-sm text-neutral-400">Name</th>
|
<th className="px-6 py-4 text-left text-sm text-neutral-400">Name</th>
|
||||||
<th className="px-6 py-4 text-left text-sm text-neutral-400">Email</th>
|
<th className="px-6 py-4 text-left text-sm text-neutral-400">Email</th>
|
||||||
<th className="px-6 py-4 text-left text-sm text-neutral-400">Role</th>
|
<th className="px-6 py-4 text-left text-sm text-neutral-400">Role</th>
|
||||||
<th className="px-6 py-4 text-left text-sm text-neutral-400">Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -335,20 +261,6 @@ function AdminPanel() {
|
|||||||
{user.isAdmin ? 'Admin' : 'User'}
|
{user.isAdmin ? 'Admin' : 'User'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setResetPasswordModal({
|
|
||||||
isOpen: true,
|
|
||||||
userId: user.id,
|
|
||||||
userName: user.name,
|
|
||||||
newPassword: '',
|
|
||||||
})}
|
|
||||||
className="flex items-center space-x-1 text-neutral-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
<FiKey className="w-4 h-4" />
|
|
||||||
<span>Reset Password</span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -1,103 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { forgotPassword } from '../../services/api';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
export default function ForgotPassword() {
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [status, setStatus] = useState({ type: '', message: '' });
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
setStatus({ type: '', message: '' });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await forgotPassword(email);
|
|
||||||
setStatus({
|
|
||||||
type: 'success',
|
|
||||||
message: 'If an account exists with this email, you will receive password reset instructions.'
|
|
||||||
});
|
|
||||||
setEmail('');
|
|
||||||
} catch (error) {
|
|
||||||
setStatus({
|
|
||||||
type: 'error',
|
|
||||||
message: 'Failed to process request. Please try again.'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-900 to-primary-800 py-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="max-w-md w-full space-y-8 bg-primary-800/50 backdrop-blur-lg p-8 rounded-xl shadow-xl">
|
|
||||||
<div>
|
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-white">
|
|
||||||
Forgot your password?
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2 text-center text-sm text-neutral-400">
|
|
||||||
Enter your email address and we'll send you instructions to reset your password.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
||||||
{status.message && (
|
|
||||||
<div
|
|
||||||
className={`p-4 rounded-lg ${
|
|
||||||
status.type === 'success'
|
|
||||||
? 'bg-green-500/10 border border-green-500/20 text-green-200'
|
|
||||||
: 'bg-red-500/10 border border-red-500/20 text-red-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{status.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="sr-only">
|
|
||||||
Email address
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
required
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="appearance-none relative block w-full px-3 py-2 border
|
|
||||||
border-primary-600 bg-primary-700/30 placeholder-neutral-400
|
|
||||||
text-white rounded-lg focus:outline-none focus:ring-primary-500
|
|
||||||
focus:border-primary-500 focus:z-10 sm:text-sm"
|
|
||||||
placeholder="Email address"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="group relative w-full flex justify-center py-2 px-4 border
|
|
||||||
border-transparent text-sm font-medium rounded-lg text-white
|
|
||||||
bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2
|
|
||||||
focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50
|
|
||||||
disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
{loading ? 'Sending...' : 'Send Reset Instructions'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="font-medium text-primary-400 hover:text-primary-300 transition-colors"
|
|
||||||
>
|
|
||||||
Back to login
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,157 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
|
|
||||||
import { resetPassword } from '../../services/api';
|
|
||||||
|
|
||||||
export default function ResetPassword() {
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
});
|
|
||||||
const [status, setStatus] = useState({ type: '', message: '' });
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const token = searchParams.get('token');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!token) {
|
|
||||||
navigate('/forgot-password');
|
|
||||||
}
|
|
||||||
}, [token, navigate]);
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (formData.password !== formData.confirmPassword) {
|
|
||||||
setStatus({
|
|
||||||
type: 'error',
|
|
||||||
message: 'Passwords do not match'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formData.password.length < 6) {
|
|
||||||
setStatus({
|
|
||||||
type: 'error',
|
|
||||||
message: 'Password must be at least 6 characters long'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setStatus({ type: '', message: '' });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await resetPassword(token, formData.password);
|
|
||||||
setStatus({
|
|
||||||
type: 'success',
|
|
||||||
message: 'Password reset successful. You can now login with your new password.'
|
|
||||||
});
|
|
||||||
setFormData({ password: '', confirmPassword: '' });
|
|
||||||
// Redirect to login after 3 seconds
|
|
||||||
setTimeout(() => navigate('/login'), 3000);
|
|
||||||
} catch (error) {
|
|
||||||
setStatus({
|
|
||||||
type: 'error',
|
|
||||||
message: error.response?.data?.message || 'Failed to reset password. Please try again.'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!token) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-900 to-primary-800 py-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="max-w-md w-full space-y-8 bg-primary-800/50 backdrop-blur-lg p-8 rounded-xl shadow-xl">
|
|
||||||
<div>
|
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-white">
|
|
||||||
Reset your password
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2 text-center text-sm text-neutral-400">
|
|
||||||
Enter your new password below
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
||||||
{status.message && (
|
|
||||||
<div
|
|
||||||
className={`p-4 rounded-lg ${
|
|
||||||
status.type === 'success'
|
|
||||||
? 'bg-green-500/10 border border-green-500/20 text-green-200'
|
|
||||||
: 'bg-red-500/10 border border-red-500/20 text-red-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{status.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="sr-only">
|
|
||||||
New Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
value={formData.password}
|
|
||||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
||||||
className="appearance-none relative block w-full px-3 py-2 border
|
|
||||||
border-primary-600 bg-primary-700/30 placeholder-neutral-400
|
|
||||||
text-white rounded-lg focus:outline-none focus:ring-primary-500
|
|
||||||
focus:border-primary-500 focus:z-10 sm:text-sm"
|
|
||||||
placeholder="New password"
|
|
||||||
minLength={6}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="confirmPassword" className="sr-only">
|
|
||||||
Confirm Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="confirmPassword"
|
|
||||||
name="confirmPassword"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
|
||||||
className="appearance-none relative block w-full px-3 py-2 border
|
|
||||||
border-primary-600 bg-primary-700/30 placeholder-neutral-400
|
|
||||||
text-white rounded-lg focus:outline-none focus:ring-primary-500
|
|
||||||
focus:border-primary-500 focus:z-10 sm:text-sm"
|
|
||||||
placeholder="Confirm new password"
|
|
||||||
minLength={6}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="group relative w-full flex justify-center py-2 px-4 border
|
|
||||||
border-transparent text-sm font-medium rounded-lg text-white
|
|
||||||
bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2
|
|
||||||
focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50
|
|
||||||
disabled:cursor-not-allowed transition-colors"
|
|
||||||
>
|
|
||||||
{loading ? 'Resetting...' : 'Reset Password'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<Link
|
|
||||||
to="/login"
|
|
||||||
className="font-medium text-primary-400 hover:text-primary-300 transition-colors"
|
|
||||||
>
|
|
||||||
Back to login
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -2,7 +2,6 @@ import { useState } from 'react';
|
|||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button, Card, Input, FormGroup, Label, ErrorMessage } from '../UI';
|
import { Button, Card, Input, FormGroup, Label, ErrorMessage } from '../UI';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
@ -78,14 +77,14 @@ export default function Login() {
|
|||||||
<Label htmlFor="password" className="text-white">
|
<Label htmlFor="password" className="text-white">
|
||||||
Password
|
Password
|
||||||
</Label>
|
</Label>
|
||||||
{/* <Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => navigate('/forgot-password')}
|
onClick={() => navigate('/forgot-password')}
|
||||||
className="text-neutral-300 hover:text-white"
|
className="text-neutral-300 hover:text-white"
|
||||||
>
|
>
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</Button> */}
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
@ -100,33 +99,46 @@ export default function Login() {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<div>
|
<Button
|
||||||
<button
|
type="submit"
|
||||||
type="submit"
|
variant="primary"
|
||||||
disabled={isLoading}
|
fullWidth
|
||||||
className="group relative w-full flex justify-center py-2 px-4 border
|
disabled={isLoading}
|
||||||
border-transparent text-sm font-medium rounded-lg text-white
|
className="bg-primary-600 hover:bg-primary-700 focus:ring-primary-500
|
||||||
bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2
|
disabled:bg-primary-800 disabled:cursor-not-allowed
|
||||||
focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50
|
transition-all duration-200 ease-in-out"
|
||||||
disabled:cursor-not-allowed transition-colors"
|
>
|
||||||
>
|
{isLoading ? (
|
||||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
<span className="flex items-center justify-center">
|
||||||
</button>
|
<svg
|
||||||
</div>
|
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<div className="flex items-center justify-between">
|
fill="none"
|
||||||
<div className="text-sm">
|
viewBox="0 0 24 24"
|
||||||
<Link
|
>
|
||||||
to="/forgot-password"
|
<circle
|
||||||
className="font-medium text-primary-400 hover:text-primary-300 transition-colors"
|
className="opacity-25"
|
||||||
>
|
cx="12"
|
||||||
Forgot your password?
|
cy="12"
|
||||||
</Link>
|
r="10"
|
||||||
</div>
|
stroke="currentColor"
|
||||||
</div>
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Signing in...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'Sign in'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* <div className="mt-6 text-center">
|
<div className="mt-6 text-center">
|
||||||
<p className="text-sm text-neutral-300">
|
<p className="text-sm text-neutral-300">
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
<Button
|
<Button
|
||||||
@ -138,7 +150,7 @@ export default function Login() {
|
|||||||
Sign up
|
Sign up
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
</div> */}
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -98,10 +98,5 @@ export const getUserInfo = () => api.get('/auth/user-info');
|
|||||||
export const getAllDocuments = () => api.get('/admin/documents');
|
export const getAllDocuments = () => api.get('/admin/documents');
|
||||||
// export const getSharedDocuments = (userId) => api.get(`/documents/shared/${userId}`);
|
// export const getSharedDocuments = (userId) => api.get(`/documents/shared/${userId}`);
|
||||||
export const getAllUsers = () => api.get('/admin/users');
|
export const getAllUsers = () => api.get('/admin/users');
|
||||||
export const resetUserPassword = (userId, newPassword) => api.post(`/admin/users/${userId}/reset-password`, { password: newPassword });
|
|
||||||
|
|
||||||
// Password reset endpoints
|
|
||||||
export const forgotPassword = (email) => api.post('/auth/forgot-password', { email });
|
|
||||||
export const resetPassword = (token, newPassword) => api.post('/auth/reset-password', { token, newPassword });
|
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user