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
|
backend/imk-backend/test
|
||||||
frontend/imk/node_modules
|
frontend/imk/node_modules
|
||||||
frontend/imk/dist
|
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 { 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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_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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user