download working

This commit is contained in:
dimitar 2024-11-02 00:08:04 +01:00
parent ed1a580b09
commit bc47e1d39a
11 changed files with 544 additions and 355 deletions

2
.gitignore vendored
View File

@ -5,3 +5,5 @@ backend/imk-backend/dist
backend/imk-backend/test backend/imk-backend/test
frontend/imk/node_modules frontend/imk/node_modules
frontend/imk/dist frontend/imk/dist
node_modules

View File

@ -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 { DocumentsService } from './documents.service';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
@Controller('documents') @Controller('documents')
export class DocumentsController { export class DocumentsController {
constructor(private readonly documentsService: DocumentsService) {} constructor(private readonly documentsService: DocumentsService) {}
@Get('shared/:userId') @Get('shared/:userId')
async getSharedDocuments(@Param('userId') userId: string) { async getSharedDocuments(@Param('userId') userId: string) {
console.log('userId', userId);
return this.documentsService.getClientDocuments(parseInt(userId)); return this.documentsService.getClientDocuments(parseInt(userId));
} }
@Get('shared/download/:key') @Get('shared/download/:key')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
async downloadDocument(@Param('key') key: string, @Req() req) { async downloadDocument(
return this.documentsService.downloadDocument(key, req.user.id); @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);
} }
} }

View File

@ -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 { PrismaService } from '../prisma/prisma.service';
// //import { Document } from '@prisma/client'; // import { S3Service } from '../s3/s3.service';
// import { Document, User } from '@prisma/client';
// @Injectable() // @Injectable()
// export class DocumentsService { // export class DocumentsService {
// downloadDocument(key: string, id: any) { // constructor(
// throw new Error('Method not implemented.'); // private prisma: PrismaService,
// } // private s3Service: S3Service,
// constructor(private readonly prisma: PrismaService) {} // ) {}
// async getClientDocuments(clientId: number) { // async findAllByClient(userId: number): Promise<Document[]> {
// // return this.prisma.document.findMany({ // const documents = await this.prisma.document.findMany({
// // where: {
// // sharedWithId: clientId,
// // },
// // include: {
// // sharedWith: {
// // select: {
// // id: true,
// // name: true,
// // email: true,
// // },
// // },
// // },
// // });
// return this.prisma.document.findMany({
// where: { // where: {
// sharedWithId: clientId, // sharedWithId: userId,
// },
// include: {
// sharedWith: {
// select: {
// id: true,
// name: true,
// email: true,
// },
// },
// }, // },
// orderBy: { // orderBy: {
// createdAt: 'desc', // 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 { Injectable, NotFoundException, UnauthorizedException } 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 { Document, User } from '@prisma/client'; import { Document, User } from '@prisma/client';
import { Readable } from 'stream';
@Injectable() @Injectable()
export class DocumentsService { export class DocumentsService {
constructor( constructor(
private prisma: PrismaService, private readonly prisma: PrismaService,
private s3Service: S3Service, private readonly s3Service: S3Service,
) {} ) {}
async findAllByClient(userId: number): Promise<Document[]> { async getClientDocuments(clientId: number) {
const documents = await this.prisma.document.findMany({ return this.prisma.document.findMany({
where: { where: {
sharedWithId: userId, sharedWithId: clientId,
},
include: {
sharedWith: {
select: {
id: true,
name: true,
email: true,
},
},
}, },
orderBy: { orderBy: {
createdAt: 'desc', 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> { async downloadDocument(s3Key: string, userId: number) {
// Verify document exists and user has access // Verify document access
const document = await this.prisma.document.findFirst({ const document = await this.prisma.document.findFirst({
where: { where: {
s3Key: s3Key, s3Key: s3Key,
@ -106,65 +206,31 @@ export class DocumentsService {
} }
try { try {
// Get document stream from S3 const s3Response = await this.s3Service.getObject(s3Key);
const fileStream = await this.s3Service.getObject(s3Key).createReadStream();
return fileStream; 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) { } catch (error) {
console.error('Error downloading document:', error); console.error('Error downloading from S3:', error);
throw new NotFoundException('Document file not found in storage'); 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,
},
},
},
});
}
} }

View File

@ -5,6 +5,7 @@ import {
DeleteObjectCommand, DeleteObjectCommand,
GetObjectCommand, GetObjectCommand,
ListObjectsCommand, ListObjectsCommand,
PutObjectCommand,
} from '@aws-sdk/client-s3'; } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Upload } from '@aws-sdk/lib-storage'; import { Upload } from '@aws-sdk/lib-storage';
@ -14,7 +15,6 @@ import { ConfigService } from '@nestjs/config';
export class S3Service { export class S3Service {
private s3Client: S3Client; private s3Client: S3Client;
private readonly logger = new Logger(S3Service.name); private readonly logger = new Logger(S3Service.name);
s3: any;
constructor(private configService: ConfigService) { constructor(private configService: ConfigService) {
this.s3Client = new S3Client({ this.s3Client = new S3Client({
@ -28,45 +28,54 @@ export class S3Service {
}); });
} }
async getObject(key: string) {
async uploadFile(file: Express.Multer.File, key: string): Promise<string> {
try { try {
const uniqueKey = `${Date.now()}-${key}`; // Ensure unique keys const command = new GetObjectCommand({
const upload = new Upload({ Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
client: this.s3Client, Key: key,
params: {
Bucket: process.env.AWS_S3_BUCKET_NAME,
Key: uniqueKey,
Body: file.buffer,
},
}); });
const result = await upload.done(); const response = await this.s3Client.send(command);
console.log(`File uploaded successfully: ${uniqueKey}`); return response;
console.log(result);
return uniqueKey;
} catch (error) { } 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; throw error;
} }
} }
async deleteFile(key: string): Promise<void> { async deleteFile(key: string): Promise<void> {
const command = new DeleteObjectCommand({ try {
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), const command = new DeleteObjectCommand({
Key: key, Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
}); Key: key,
});
await this.s3Client.send(command); await this.s3Client.send(command);
} } catch (error) {
getObject(key: string) { this.logger.error(`Error deleting file from S3: ${error.message}`);
return this.s3.getObject({ throw error;
Bucket: process.env.AWS_S3_BUCKET, }
Key: key,
});
} }
async getFileUrl(key: string): Promise<string> { async getFileUrl(key: string): Promise<string> {
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
Key: key, Key: key,
@ -75,21 +84,20 @@ export class S3Service {
const url = await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); // URL expires in 1 hour const url = await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); // URL expires in 1 hour
return url; return url;
} }
async testConnection(): Promise<boolean> { async testConnection(): Promise<boolean> {
try { try {
const command = new ListObjectsCommand({ await this.getObject('test-connection');
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.`,
);
return true; return true;
} catch (error) { } 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); this.logger.error('Failed to connect to S3', error);
return false; return false;
} }
} }
} }

View File

@ -1,4 +1,5 @@
import './App.css' // import './App.css'
import { AuthProvider } from './hooks/useAuth';
import Navbar from './components/navbar/Navbar' import Navbar from './components/navbar/Navbar'
import { BrowserRouter, Routes, Route } from "react-router-dom"; import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from './pages/homepage/Home' 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 Certificates from './components/Certificates/Certificates.jsx';
import Clients from './components/clients/clients'; import Clients from './components/clients/clients';
import AdminPanel from './components/adminPanel/AdminPanel'; import AdminPanel from './components/adminPanel/AdminPanel';
import Login from './components/login/login';
import Dashboard from './components/dashboard/Dashboard'; import Dashboard from './components/dashboard/Dashboard';
import Login from './components/login/login';
import ProtectedRoute from './components/protectedRoute/ProtectedRoute'; import ProtectedRoute from './components/protectedRoute/ProtectedRoute';
function App() { function App() {
return ( return (
<div > <AuthProvider>
<BrowserRouter > <div >
<BrowserRouter >
<Navbar /> <Navbar />
<Routes> <Routes>
<Route path='/' element={<Home />} /> <Route path='/' element={<Home />} />
@ -31,9 +33,13 @@ function App() {
<Route path='/ultrasound' element={<UltraSound />} /> <Route path='/ultrasound' element={<UltraSound />} />
<Route path='/gallery' element={<Gallery />} /> <Route path='/gallery' element={<Gallery />} />
<Route path='/certificates' element={<Certificates />} /> <Route path='/certificates' element={<Certificates />} />
<Route path='/clients' element={<Clients />} /> <Route path='/clients' element={
<ProtectedRoute>
<Clients />
</ProtectedRoute>
} />
<Route path='/admin' element={ <Route path='/admin' element={
<ProtectedRoute adminOnly={true}> <ProtectedRoute>
<AdminPanel /> <AdminPanel />
</ProtectedRoute> </ProtectedRoute>
} /> } />
@ -45,8 +51,9 @@ function App() {
} /> } />
</Routes> </Routes>
<Footer /> <Footer />
</BrowserRouter> </BrowserRouter>
</div> </div>
</AuthProvider>
) )
} }

View File

@ -4,6 +4,7 @@ import { format } from 'date-fns';
import { FiFolder, FiDownload, FiChevronRight, FiChevronDown } from 'react-icons/fi'; import { FiFolder, FiDownload, FiChevronRight, FiChevronDown } from 'react-icons/fi';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
import { getSharedDocuments } from '../../services/api'; import { getSharedDocuments } from '../../services/api';
import api from '../../services/api';
function Dashboard() { function Dashboard() {
const [documents, setDocuments] = useState([]); const [documents, setDocuments] = useState([]);
@ -42,16 +43,52 @@ function Dashboard() {
}, {}); }, {});
}; };
const handleDownload = async (s3Key, fileName) => { // const handleDownload = async (s3Key, fileName) => {
try { // try {
window.open(`http://localhost:3000/documents/shared/download/${s3Key}`, '_blank'); // const response = await api.get(`/documents/shared/download/${s3Key}`, {
} catch (err) { // responseType: 'blob',
console.error('Error downloading document:', err); // });
alert('Failed to download document. Please try again.'); // 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 => ({ setExpandedFolders(prev => ({
...prev, ...prev,
[folderName]: !prev[folderName] [folderName]: !prev[folderName]

View File

@ -1,157 +1,73 @@
import { useState } from 'react'; import { useState } from 'react';
import { useAuth } from '../../hooks/useAuth';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import axios from 'axios';
const Login = () => { const Login = () => {
const navigate = useNavigate(); const [username, setUsername] = useState(''); // Changed from email to username
const [formData, setFormData] = useState({ const [password, setPassword] = useState('');
username: '',
password: ''
});
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false); const { login } = useAuth();
const navigate = useNavigate();
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
setError('');
};
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true);
setError(''); setError('');
try { try {
const response = await axios.post('http://localhost:3000/auth/login', formData, { await login(username, password); // Changed to pass username and password separately
headers: { navigate('/dashboard');
'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');
}
} catch (err) { } catch (err) {
setError( setError('Invalid credentials');
err.response?.data?.message ||
'Неуспешна најава. Проверете ги вашите податоци.'
);
} finally {
setIsLoading(false);
} }
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white flex items-center justify-center px-4"> <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<motion.div <div className="max-w-md w-full space-y-8">
initial={{ opacity: 0, y: 20 }} <div>
animate={{ opacity: 1, y: 0 }} <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
className="max-w-md w-full space-y-8 bg-white p-8 rounded-xl shadow-lg" Sign in to your account
>
{/* Header */}
<div className="text-center">
<h2 className="text-3xl font-bold text-gray-900">
Најавете се
</h2> </h2>
<p className="mt-2 text-sm text-gray-600">
Внесете ги вашите податоци за најава
</p>
</div> </div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
{/* Form */} {error && (
<form onSubmit={handleSubmit} className="mt-8 space-y-6"> <div className="text-red-500 text-center text-sm">
<div className="space-y-4"> {error}
{/* Username Field */} </div>
)}
<div className="rounded-md shadow-sm -space-y-px">
<div> <div>
<label
htmlFor="username"
className="block text-sm font-medium text-gray-700"
>
Корисничко име
</label>
<input <input
id="username"
name="username"
type="text" type="text"
required required
value={formData.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"
onChange={handleInputChange} placeholder="Username"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm value={username}
focus:border-blue-500 focus:ring-blue-500 sm:text-sm onChange={(e) => setUsername(e.target.value)}
px-3 py-2 border"
aria-label="Username"
/> />
</div> </div>
{/* Password Field */}
<div> <div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700"
>
Лозинка
</label>
<input <input
id="password"
name="password"
type="password" type="password"
required required
value={formData.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"
onChange={handleInputChange} placeholder="Password"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm value={password}
focus:border-blue-500 focus:ring-blue-500 sm:text-sm onChange={(e) => setPassword(e.target.value)}
px-3 py-2 border"
aria-label="Password"
/> />
</div> </div>
</div> </div>
{/* Error Message */} <div>
{error && ( <button
<div type="submit"
className="text-red-500 text-sm text-center bg-red-50 p-2 rounded" 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"
role="alert"
> >
{error} Sign in
</div> </button>
)} </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>
</form> </form>
</motion.div> </div>
</div> </div>
); );
}; };

View File

@ -1,23 +1,22 @@
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
// eslint-disable-next-line react/prop-types import { useNavigate } from 'react-router-dom';
function ProtectedRoute({ children, adminOnly = false }) { import { useEffect } from 'react';
const { user, loading } = useAuth();
const location = useLocation();
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>; return <div>Loading...</div>;
} }
if (!user) { return user ? children : null;
return <Navigate to="/login" state={{ from: location }} replace />; };
}
if (adminOnly && !user.isAdmin) {
return <Navigate to="/dashboard" replace />;
}
return children;
}
export default ProtectedRoute; export default ProtectedRoute;

View File

@ -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 };
}

View 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;
};

View File

@ -4,17 +4,102 @@ import axios from 'axios';
const API_URL = 'http://localhost:3000'; const API_URL = 'http://localhost:3000';
const api = axios.create({ const api = axios.create({
baseURL: API_URL baseURL: API_URL,
// withCredentials: true,
}); });
api.interceptors.request.use((config) => { // const api = axios.create({
const token = localStorage.getItem('token'); // baseURL: 'http://localhost:3000',
if (token) { // });
config.headers.Authorization = `Bearer ${token}`;
// 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); // 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) => { export const createUser = (userData) => {
return api.post('/admin/users', { return api.post('/admin/users', {
name: userData.name, name: userData.name,