download working
This commit is contained in:
parent
ed1a580b09
commit
bc47e1d39a
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,3 +5,5 @@ backend/imk-backend/dist
|
||||
backend/imk-backend/test
|
||||
frontend/imk/node_modules
|
||||
frontend/imk/dist
|
||||
node_modules
|
||||
|
||||
|
||||
@ -1,19 +1,33 @@
|
||||
import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common';
|
||||
|
||||
import { Controller, Get, Param, Req, Res, UseGuards } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { DocumentsService } from './documents.service';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
|
||||
@Controller('documents')
|
||||
export class DocumentsController {
|
||||
constructor(private readonly documentsService: DocumentsService) {}
|
||||
|
||||
@Get('shared/:userId')
|
||||
async getSharedDocuments(@Param('userId') userId: string) {
|
||||
console.log('userId', userId);
|
||||
return this.documentsService.getClientDocuments(parseInt(userId));
|
||||
}
|
||||
|
||||
@Get('shared/download/:key')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async downloadDocument(@Param('key') key: string, @Req() req) {
|
||||
return this.documentsService.downloadDocument(key, req.user.id);
|
||||
async downloadDocument(
|
||||
@Param('key') key: string,
|
||||
@Req() req,
|
||||
@Res() res: Response
|
||||
) {
|
||||
const file = await this.documentsService.downloadDocument(key, req.user.id);
|
||||
|
||||
res.set({
|
||||
'Content-Type': file.contentType,
|
||||
'Content-Length': file.contentLength,
|
||||
'Content-Disposition': `attachment; filename="${file.fileName}"`,
|
||||
});
|
||||
|
||||
res.send(file.buffer);
|
||||
}
|
||||
}
|
||||
@ -1,99 +1,199 @@
|
||||
// import { Injectable } from '@nestjs/common';
|
||||
// // import { Injectable } from '@nestjs/common';
|
||||
// // import { PrismaService } from '../prisma/prisma.service';
|
||||
// // //import { Document } from '@prisma/client';
|
||||
|
||||
// // @Injectable()
|
||||
// // export class DocumentsService {
|
||||
// // downloadDocument(key: string, id: any) {
|
||||
// // throw new Error('Method not implemented.');
|
||||
// // }
|
||||
// // constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
// // async getClientDocuments(clientId: number) {
|
||||
// // // return this.prisma.document.findMany({
|
||||
// // // where: {
|
||||
// // // sharedWithId: clientId,
|
||||
// // // },
|
||||
// // // include: {
|
||||
// // // sharedWith: {
|
||||
// // // select: {
|
||||
// // // id: true,
|
||||
// // // name: true,
|
||||
// // // email: true,
|
||||
// // // },
|
||||
// // // },
|
||||
// // // },
|
||||
// // // });
|
||||
// // return this.prisma.document.findMany({
|
||||
// // where: {
|
||||
// // sharedWithId: clientId,
|
||||
// // },
|
||||
// // orderBy: {
|
||||
// // createdAt: 'desc',
|
||||
// // },
|
||||
// // });
|
||||
// // }
|
||||
// // }
|
||||
// import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||
// import { PrismaService } from '../prisma/prisma.service';
|
||||
// //import { Document } from '@prisma/client';
|
||||
// import { S3Service } from '../s3/s3.service';
|
||||
// import { Document, User } from '@prisma/client';
|
||||
|
||||
// @Injectable()
|
||||
// export class DocumentsService {
|
||||
// downloadDocument(key: string, id: any) {
|
||||
// throw new Error('Method not implemented.');
|
||||
// }
|
||||
// constructor(private readonly prisma: PrismaService) {}
|
||||
// constructor(
|
||||
// private prisma: PrismaService,
|
||||
// private s3Service: S3Service,
|
||||
// ) {}
|
||||
|
||||
// async getClientDocuments(clientId: number) {
|
||||
// // return this.prisma.document.findMany({
|
||||
// // where: {
|
||||
// // sharedWithId: clientId,
|
||||
// // },
|
||||
// // include: {
|
||||
// // sharedWith: {
|
||||
// // select: {
|
||||
// // id: true,
|
||||
// // name: true,
|
||||
// // email: true,
|
||||
// // },
|
||||
// // },
|
||||
// // },
|
||||
// // });
|
||||
// return this.prisma.document.findMany({
|
||||
// async findAllByClient(userId: number): Promise<Document[]> {
|
||||
// const documents = await this.prisma.document.findMany({
|
||||
// where: {
|
||||
// sharedWithId: clientId,
|
||||
// sharedWithId: userId,
|
||||
// },
|
||||
// include: {
|
||||
// sharedWith: {
|
||||
// select: {
|
||||
// id: true,
|
||||
// name: true,
|
||||
// email: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// orderBy: {
|
||||
// createdAt: 'desc',
|
||||
// },
|
||||
// });
|
||||
|
||||
// return documents;
|
||||
// }
|
||||
// async getClientDocuments(clientId: number) {
|
||||
// // return this.prisma.document.findMany({
|
||||
// // where: {
|
||||
// // sharedWithId: clientId,
|
||||
// // },
|
||||
// // include: {
|
||||
// // sharedWith: {
|
||||
// // select: {
|
||||
// // id: true,
|
||||
// // name: true,
|
||||
// // email: true,
|
||||
// // },
|
||||
// // },
|
||||
// // },
|
||||
// // });
|
||||
// return this.prisma.document.findMany({
|
||||
// where: {
|
||||
// sharedWithId: clientId,
|
||||
// },
|
||||
// orderBy: {
|
||||
// createdAt: 'desc',
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// async downloadDocument(s3Key: string, userId: number): Promise<any> {
|
||||
// // Verify document exists and user has access
|
||||
// const document = await this.prisma.document.findFirst({
|
||||
// where: {
|
||||
// s3Key: s3Key,
|
||||
// sharedWithId: userId,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!document) {
|
||||
// throw new NotFoundException('Document not found or access denied');
|
||||
// }
|
||||
|
||||
// try {
|
||||
// // Get document stream from S3
|
||||
// const fileStream = await this.s3Service.getObject(s3Key).createReadStream();
|
||||
// return fileStream;
|
||||
// } catch (error) {
|
||||
// console.error('Error downloading document:', error);
|
||||
// throw new NotFoundException('Document file not found in storage');
|
||||
// }
|
||||
// }
|
||||
|
||||
// async getAllDocuments(): Promise<Document[]> {
|
||||
// return this.prisma.document.findMany({
|
||||
// include: {
|
||||
// sharedWith: {
|
||||
// select: {
|
||||
// id: true,
|
||||
// name: true,
|
||||
// email: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// orderBy: {
|
||||
// createdAt: 'desc',
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// async shareDocument(documentId: number, userId: number): Promise<Document> {
|
||||
// const document = await this.prisma.document.update({
|
||||
// where: { id: documentId },
|
||||
// data: {
|
||||
// sharedWithId: userId,
|
||||
// },
|
||||
// include: {
|
||||
// sharedWith: {
|
||||
// select: {
|
||||
// id: true,
|
||||
// name: true,
|
||||
// email: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
// return document;
|
||||
// }
|
||||
|
||||
// async updateStatus(documentId: number, status: string): Promise<Document> {
|
||||
// return this.prisma.document.update({
|
||||
// where: { id: documentId },
|
||||
// data: { status },
|
||||
// include: {
|
||||
// sharedWith: {
|
||||
// select: {
|
||||
// id: true,
|
||||
// name: true,
|
||||
// email: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { S3Service } from '../s3/s3.service';
|
||||
import { Document, User } from '@prisma/client';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentsService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private s3Service: S3Service,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly s3Service: S3Service,
|
||||
) {}
|
||||
|
||||
async findAllByClient(userId: number): Promise<Document[]> {
|
||||
const documents = await this.prisma.document.findMany({
|
||||
async getClientDocuments(clientId: number) {
|
||||
return this.prisma.document.findMany({
|
||||
where: {
|
||||
sharedWithId: userId,
|
||||
},
|
||||
include: {
|
||||
sharedWith: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
sharedWithId: clientId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
return documents;
|
||||
}
|
||||
async getClientDocuments(clientId: number) {
|
||||
// return this.prisma.document.findMany({
|
||||
// where: {
|
||||
// sharedWithId: clientId,
|
||||
// },
|
||||
// include: {
|
||||
// sharedWith: {
|
||||
// select: {
|
||||
// id: true,
|
||||
// name: true,
|
||||
// email: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
return this.prisma.document.findMany({
|
||||
where: {
|
||||
sharedWithId: clientId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async downloadDocument(s3Key: string, userId: number): Promise<any> {
|
||||
// Verify document exists and user has access
|
||||
async downloadDocument(s3Key: string, userId: number) {
|
||||
// Verify document access
|
||||
const document = await this.prisma.document.findFirst({
|
||||
where: {
|
||||
s3Key: s3Key,
|
||||
@ -106,65 +206,31 @@ export class DocumentsService {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get document stream from S3
|
||||
const fileStream = await this.s3Service.getObject(s3Key).createReadStream();
|
||||
return fileStream;
|
||||
const s3Response = await this.s3Service.getObject(s3Key);
|
||||
|
||||
if (!s3Response.Body) {
|
||||
throw new Error('No file content received from S3');
|
||||
}
|
||||
|
||||
// Convert the response body to a buffer
|
||||
const streamBody = s3Response.Body as Readable;
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
for await (const chunk of streamBody) {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
|
||||
const fileBuffer = Buffer.concat(chunks);
|
||||
|
||||
return {
|
||||
buffer: fileBuffer,
|
||||
contentType: s3Response.ContentType || 'application/octet-stream',
|
||||
contentLength: s3Response.ContentLength,
|
||||
fileName: s3Key.split('/').pop() || 'download'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error downloading document:', error);
|
||||
throw new NotFoundException('Document file not found in storage');
|
||||
console.error('Error downloading from S3:', error);
|
||||
throw new NotFoundException('Failed to download file');
|
||||
}
|
||||
}
|
||||
|
||||
async getAllDocuments(): Promise<Document[]> {
|
||||
return this.prisma.document.findMany({
|
||||
include: {
|
||||
sharedWith: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async shareDocument(documentId: number, userId: number): Promise<Document> {
|
||||
const document = await this.prisma.document.update({
|
||||
where: { id: documentId },
|
||||
data: {
|
||||
sharedWithId: userId,
|
||||
},
|
||||
include: {
|
||||
sharedWith: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
async updateStatus(documentId: number, status: string): Promise<Document> {
|
||||
return this.prisma.document.update({
|
||||
where: { id: documentId },
|
||||
data: { status },
|
||||
include: {
|
||||
sharedWith: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import {
|
||||
DeleteObjectCommand,
|
||||
GetObjectCommand,
|
||||
ListObjectsCommand,
|
||||
PutObjectCommand,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { Upload } from '@aws-sdk/lib-storage';
|
||||
@ -14,7 +15,6 @@ import { ConfigService } from '@nestjs/config';
|
||||
export class S3Service {
|
||||
private s3Client: S3Client;
|
||||
private readonly logger = new Logger(S3Service.name);
|
||||
s3: any;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.s3Client = new S3Client({
|
||||
@ -28,45 +28,54 @@ export class S3Service {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async uploadFile(file: Express.Multer.File, key: string): Promise<string> {
|
||||
async getObject(key: string) {
|
||||
try {
|
||||
const uniqueKey = `${Date.now()}-${key}`; // Ensure unique keys
|
||||
const upload = new Upload({
|
||||
client: this.s3Client,
|
||||
params: {
|
||||
Bucket: process.env.AWS_S3_BUCKET_NAME,
|
||||
Key: uniqueKey,
|
||||
Body: file.buffer,
|
||||
},
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
||||
Key: key,
|
||||
});
|
||||
|
||||
const result = await upload.done();
|
||||
console.log(`File uploaded successfully: ${uniqueKey}`);
|
||||
console.log(result);
|
||||
return uniqueKey;
|
||||
const response = await this.s3Client.send(command);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`Error uploading file: ${error.message}`);
|
||||
this.logger.error(`Error getting object from S3: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async uploadFile(file: Express.Multer.File, folder: string): Promise<string> {
|
||||
try {
|
||||
const key = `${folder}/${Date.now()}-${file.originalname}`;
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
||||
Key: key,
|
||||
Body: file.buffer,
|
||||
ContentType: file.mimetype,
|
||||
});
|
||||
|
||||
await this.s3Client.send(command);
|
||||
return key;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error uploading file to S3: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(key: string): Promise<void> {
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
||||
Key: key,
|
||||
});
|
||||
try {
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
||||
Key: key,
|
||||
});
|
||||
|
||||
await this.s3Client.send(command);
|
||||
}
|
||||
getObject(key: string) {
|
||||
return this.s3.getObject({
|
||||
Bucket: process.env.AWS_S3_BUCKET,
|
||||
Key: key,
|
||||
});
|
||||
await this.s3Client.send(command);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting file from S3: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getFileUrl(key: string): Promise<string> {
|
||||
async getFileUrl(key: string): Promise<string> {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
||||
Key: key,
|
||||
@ -75,21 +84,20 @@ export class S3Service {
|
||||
const url = await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); // URL expires in 1 hour
|
||||
return url;
|
||||
}
|
||||
|
||||
async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
const command = new ListObjectsCommand({
|
||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
||||
MaxKeys: 1,
|
||||
});
|
||||
|
||||
const response = await this.s3Client.send(command);
|
||||
this.logger.log(
|
||||
`Successfully connected to S3. Bucket contains ${response.Contents?.length || 0} objects.`,
|
||||
);
|
||||
await this.getObject('test-connection');
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.name === 'NoSuchKey') {
|
||||
// This is expected as we're just testing connection
|
||||
return true;
|
||||
}
|
||||
this.logger.error('Failed to connect to S3', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import './App.css'
|
||||
// import './App.css'
|
||||
import { AuthProvider } from './hooks/useAuth';
|
||||
import Navbar from './components/navbar/Navbar'
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Home from './pages/homepage/Home'
|
||||
@ -12,15 +13,16 @@ import Gallery from './components/gallery/Gallery.jsx';
|
||||
import Certificates from './components/Certificates/Certificates.jsx';
|
||||
import Clients from './components/clients/clients';
|
||||
import AdminPanel from './components/adminPanel/AdminPanel';
|
||||
import Login from './components/login/login';
|
||||
import Dashboard from './components/dashboard/Dashboard';
|
||||
import Login from './components/login/login';
|
||||
import ProtectedRoute from './components/protectedRoute/ProtectedRoute';
|
||||
|
||||
function App() {
|
||||
|
||||
return (
|
||||
<div >
|
||||
<BrowserRouter >
|
||||
<AuthProvider>
|
||||
<div >
|
||||
<BrowserRouter >
|
||||
<Navbar />
|
||||
<Routes>
|
||||
<Route path='/' element={<Home />} />
|
||||
@ -31,9 +33,13 @@ function App() {
|
||||
<Route path='/ultrasound' element={<UltraSound />} />
|
||||
<Route path='/gallery' element={<Gallery />} />
|
||||
<Route path='/certificates' element={<Certificates />} />
|
||||
<Route path='/clients' element={<Clients />} />
|
||||
<Route path='/clients' element={
|
||||
<ProtectedRoute>
|
||||
<Clients />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path='/admin' element={
|
||||
<ProtectedRoute adminOnly={true}>
|
||||
<ProtectedRoute>
|
||||
<AdminPanel />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
@ -45,8 +51,9 @@ function App() {
|
||||
} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { format } from 'date-fns';
|
||||
import { FiFolder, FiDownload, FiChevronRight, FiChevronDown } from 'react-icons/fi';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { getSharedDocuments } from '../../services/api';
|
||||
import api from '../../services/api';
|
||||
|
||||
function Dashboard() {
|
||||
const [documents, setDocuments] = useState([]);
|
||||
@ -42,16 +43,52 @@ function Dashboard() {
|
||||
}, {});
|
||||
};
|
||||
|
||||
const handleDownload = async (s3Key, fileName) => {
|
||||
try {
|
||||
window.open(`http://localhost:3000/documents/shared/download/${s3Key}`, '_blank');
|
||||
} catch (err) {
|
||||
console.error('Error downloading document:', err);
|
||||
alert('Failed to download document. Please try again.');
|
||||
}
|
||||
};
|
||||
// const handleDownload = async (s3Key, fileName) => {
|
||||
// try {
|
||||
// const response = await api.get(`/documents/shared/download/${s3Key}`, {
|
||||
// responseType: 'blob',
|
||||
// });
|
||||
// const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
// const link = document.createElement('a');
|
||||
// link.href = url;
|
||||
// link.setAttribute('download', fileName); // or use the actual filename
|
||||
// document.body.appendChild(link);
|
||||
// link.click();
|
||||
// link.parentNode.removeChild(link);
|
||||
// } catch (err) {
|
||||
// console.error('Error downloading document:', err);
|
||||
// alert('Failed to download document. Please try again.');
|
||||
// }
|
||||
// };
|
||||
// const groupDocumentsByCompanyAndDate = (docs) => {
|
||||
// return docs.reduce((acc, doc) => {
|
||||
// const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`;
|
||||
// if (!acc[folderName]) {
|
||||
// acc[folderName] = [];
|
||||
// }
|
||||
// acc[folderName].push(doc);
|
||||
// return acc;
|
||||
// }, {});
|
||||
// };
|
||||
|
||||
const toggleFolder = (folderName) => {
|
||||
const handleDownload = async (s3Key, fileName) => {
|
||||
try {
|
||||
const response = await api.get(`/documents/shared/download/${s3Key}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', fileName); // or use the actual filename
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode.removeChild(link);
|
||||
} catch (err) {
|
||||
console.error('Error downloading document:', err);
|
||||
alert('Failed to download document. Please try again.');
|
||||
}
|
||||
};
|
||||
const toggleFolder = (folderName) => {
|
||||
setExpandedFolders(prev => ({
|
||||
...prev,
|
||||
[folderName]: !prev[folderName]
|
||||
|
||||
@ -1,157 +1,73 @@
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import axios from 'axios';
|
||||
|
||||
const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
const [username, setUsername] = useState(''); // Changed from email to username
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
setError('');
|
||||
};
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await axios.post('http://localhost:3000/auth/login', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.access_token) {
|
||||
// Store the token
|
||||
localStorage.setItem('token', response.data.access_token);
|
||||
|
||||
// Redirect based on user role (assuming you have this info)
|
||||
const isAdmin = response.data.isAdmin; // Adjust based on your API response
|
||||
navigate(isAdmin ? '/admin' : '/dashboard');
|
||||
} else {
|
||||
setError('Invalid response from server');
|
||||
}
|
||||
await login(username, password); // Changed to pass username and password separately
|
||||
navigate('/dashboard');
|
||||
} catch (err) {
|
||||
setError(
|
||||
err.response?.data?.message ||
|
||||
'Неуспешна најава. Проверете ги вашите податоци.'
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setError('Invalid credentials');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white flex items-center justify-center px-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="max-w-md w-full space-y-8 bg-white p-8 rounded-xl shadow-lg"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl font-bold text-gray-900">
|
||||
Најавете се
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Sign in to your account
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Внесете ги вашите податоци за најава
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
|
||||
<div className="space-y-4">
|
||||
{/* Username Field */}
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="text-red-500 text-center text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Корисничко име
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
value={formData.username}
|
||||
onChange={handleInputChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm
|
||||
focus:border-blue-500 focus:ring-blue-500 sm:text-sm
|
||||
px-3 py-2 border"
|
||||
aria-label="Username"
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Лозинка
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm
|
||||
focus:border-blue-500 focus:ring-blue-500 sm:text-sm
|
||||
px-3 py-2 border"
|
||||
aria-label="Password"
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div
|
||||
className="text-red-500 text-sm text-center bg-red-50 p-2 rounded"
|
||||
role="alert"
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full flex justify-center py-3 px-4 border border-transparent
|
||||
rounded-full text-sm font-semibold text-white bg-blue-600
|
||||
hover:bg-blue-700 focus:outline-none focus:ring-2
|
||||
focus:ring-offset-2 focus:ring-blue-500 transition-colors
|
||||
duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center">
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<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"></path>
|
||||
</svg>
|
||||
Најава во тек...
|
||||
</span>
|
||||
) : (
|
||||
'Најави се'
|
||||
)}
|
||||
</button>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,23 +1,22 @@
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
// eslint-disable-next-line react/prop-types
|
||||
function ProtectedRoute({ children, adminOnly = false }) {
|
||||
const { user, loading } = useAuth();
|
||||
const location = useLocation();
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
if (loading) {
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { user, isLoading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !user) {
|
||||
navigate('/login');
|
||||
}
|
||||
}, [user, isLoading, navigate]);
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
if (adminOnly && !user.isAdmin) {
|
||||
return <Navigate to="/dashboard" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
return user ? children : null;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
@ -1,29 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getUserInfo } from '../services/api';
|
||||
import * as jwtDecode from 'jwt-decode';
|
||||
|
||||
export function useAuth() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUser() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
const decodedToken = jwtDecode.jwtDecode(token);
|
||||
const response = await getUserInfo(decodedToken.sub);
|
||||
setUser(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user info:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
return { user, loading };
|
||||
}
|
||||
84
frontend/imk/src/hooks/useAuth.jsx
Normal file
84
frontend/imk/src/hooks/useAuth.jsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { createContext, useContext, useState, useEffect } from 'react';
|
||||
import api from '../services/api';
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await api.get('/auth/user-info'); // Updated endpoint
|
||||
setUser(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user info:', error);
|
||||
localStorage.removeItem('token');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUser();
|
||||
}, []);
|
||||
const login = async (username, password) => {
|
||||
try {
|
||||
const response = await api.post('/auth/login', { username, password });
|
||||
const { access_token } = response.data; // Make sure this matches your backend response
|
||||
|
||||
localStorage.setItem('token', access_token);
|
||||
|
||||
// After setting token, fetch user info
|
||||
const userResponse = await api.get('/auth/user-info');
|
||||
setUser(userResponse.data);
|
||||
|
||||
return userResponse.data;
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// const login = async (username, password) => { // Changed parameters
|
||||
// try {
|
||||
// const response = await api.post('/auth/login', { username, password });
|
||||
// const { token } = response.data; // Updated to match backend response
|
||||
// localStorage.setItem('token', token);
|
||||
|
||||
// // Fetch user info after successful login
|
||||
// const userResponse = await api.get('/auth/user-info');
|
||||
// setUser(userResponse.data);
|
||||
|
||||
// return userResponse.data;
|
||||
// } catch (error) {
|
||||
// console.error('Login error:', error);
|
||||
// throw error;
|
||||
// }
|
||||
// };
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, isLoading, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@ -4,17 +4,102 @@ import axios from 'axios';
|
||||
const API_URL = 'http://localhost:3000';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_URL
|
||||
baseURL: API_URL,
|
||||
// withCredentials: true,
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
// const api = axios.create({
|
||||
// baseURL: 'http://localhost:3000',
|
||||
// });
|
||||
|
||||
// Add a request interceptor
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`; // Make sure this matches your backend expectation
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
return config;
|
||||
});
|
||||
);
|
||||
|
||||
export const downloadDocument = async (documentId) => {
|
||||
try {
|
||||
const response = await api.get(`/documents/download/${documentId}`, {
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Download error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Request interceptor
|
||||
// api.interceptors.request.use((config) => {
|
||||
// const token = localStorage.getItem('token');
|
||||
// if (token) {
|
||||
// config.headers.Authorization = `Bearer ${token}`;
|
||||
// }
|
||||
// return config;
|
||||
// });
|
||||
|
||||
// // Response interceptor
|
||||
// api.interceptors.response.use(
|
||||
// (response) => response,
|
||||
// (error) => {
|
||||
// if (error.response?.status === 401) {
|
||||
// localStorage.removeItem('token');
|
||||
// }
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// );
|
||||
|
||||
// const api = axios.create({
|
||||
// baseURL: API_URL,
|
||||
// withCredentials: true,
|
||||
// });
|
||||
|
||||
// api.interceptors.request.use((config) => {
|
||||
// const token = localStorage.getItem('token');
|
||||
// if (token) {
|
||||
// config.headers.Authorization = `Bearer ${token}`;
|
||||
// }
|
||||
// return config;
|
||||
// });
|
||||
// export const createUser = (userData) => api.post('/admin/users', userData);
|
||||
|
||||
// const api = axios.create({
|
||||
// baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000',
|
||||
// withCredentials: true,
|
||||
// });
|
||||
|
||||
// // Request interceptor
|
||||
// api.interceptors.request.use((config) => {
|
||||
// const token = localStorage.getItem('token');
|
||||
// if (token) {
|
||||
// config.headers.Authorization = `Bearer ${token}`;
|
||||
// }
|
||||
// return config;
|
||||
// });
|
||||
|
||||
// // Response interceptor
|
||||
// api.interceptors.response.use(
|
||||
// (response) => response,
|
||||
// (error) => {
|
||||
// if (error.response?.status === 401) {
|
||||
// localStorage.removeItem('token');
|
||||
// window.location.href = '/login';
|
||||
// }
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
// );
|
||||
export const createUser = (userData) => {
|
||||
return api.post('/admin/users', {
|
||||
name: userData.name,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user