diff --git a/backend/imk-backend/.env b/backend/imk-backend/.env index 1b35742..3c6176e 100644 --- a/backend/imk-backend/.env +++ b/backend/imk-backend/.env @@ -5,6 +5,7 @@ # See the documentation for all the connection string options: https://pris.ly/d/connection-strings DATABASE_URL="postgresql://root:admin@localhost:5432/imk?schema=public" +JWT_SECRET=some-secret AWS_REGION=EU2 AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6 AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4 diff --git a/backend/imk-backend/prisma/migrations/20241102061737_destroy/migration.sql b/backend/imk-backend/prisma/migrations/20241102061737_destroy/migration.sql new file mode 100644 index 0000000..27701e1 --- /dev/null +++ b/backend/imk-backend/prisma/migrations/20241102061737_destroy/migration.sql @@ -0,0 +1,44 @@ +/* + Warnings: + + - You are about to drop the column `content` on the `Document` table. All the data in the column will be lost. + - You are about to drop the column `sharedWithId` on the `Document` table. All the data in the column will be lost. + - Added the required column `uploadedById` to the `Document` table without a default value. This is not possible if the table is not empty. + - Added the required column `documentId` to the `Notification` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Document" DROP CONSTRAINT "Document_sharedWithId_fkey"; + +-- AlterTable +ALTER TABLE "Document" DROP COLUMN "content", +DROP COLUMN "sharedWithId", +ADD COLUMN "uploadedById" INTEGER NOT NULL, +ALTER COLUMN "status" DROP DEFAULT; + +-- AlterTable +ALTER TABLE "Notification" ADD COLUMN "documentId" INTEGER NOT NULL; + +-- CreateTable +CREATE TABLE "_SharedDocuments" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_SharedDocuments_AB_unique" ON "_SharedDocuments"("A", "B"); + +-- CreateIndex +CREATE INDEX "_SharedDocuments_B_index" ON "_SharedDocuments"("B"); + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_SharedDocuments" ADD CONSTRAINT "_SharedDocuments_A_fkey" FOREIGN KEY ("A") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_SharedDocuments" ADD CONSTRAINT "_SharedDocuments_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/imk-backend/prisma/schema.prisma b/backend/imk-backend/prisma/schema.prisma index dec4f0e..a161ff0 100644 --- a/backend/imk-backend/prisma/schema.prisma +++ b/backend/imk-backend/prisma/schema.prisma @@ -8,32 +8,36 @@ datasource db { } model Document { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) title String - content String? s3Key String - status String @default("pending") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - sharedWithId Int - sharedWith User @relation("SharedDocuments", fields: [sharedWithId], references: [id]) + status String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + uploadedById Int + uploadedBy User @relation("UploadedDocuments", fields: [uploadedById], references: [id]) + Notification Notification[] + sharedWith User[] @relation("SharedDocuments") } model User { - id Int @id @default(autoincrement()) - email String @unique - name String? - password String - isAdmin Boolean @default(false) - sharedDocuments Document[] @relation("SharedDocuments") - notifications Notification[] + id Int @id @default(autoincrement()) + email String @unique + name String? + password String + isAdmin Boolean @default(false) + uploadedDocuments Document[] @relation("UploadedDocuments") + Notification Notification[] + sharedDocuments Document[] @relation("SharedDocuments") } model Notification { - id Int @id @default(autoincrement()) - message String - read Boolean @default(false) - userId Int - createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) + id Int @id @default(autoincrement()) + message String + read Boolean @default(false) + userId Int + createdAt DateTime @default(now()) + documentId Int + document Document @relation(fields: [documentId], references: [id]) + user User @relation(fields: [userId], references: [id]) } diff --git a/backend/imk-backend/src/admin/admin.controller.ts b/backend/imk-backend/src/admin/admin.controller.ts index c37b635..e373a9a 100644 --- a/backend/imk-backend/src/admin/admin.controller.ts +++ b/backend/imk-backend/src/admin/admin.controller.ts @@ -1,3 +1,118 @@ +// import { +// Controller, +// Get, +// Post, +// Body, +// Param, +// Put, +// UseInterceptors, +// UploadedFile, +// ParseIntPipe, +// UseGuards, +// } from '@nestjs/common'; +// import { FileInterceptor } from '@nestjs/platform-express'; +// import { AdminService } from './admin.service'; +// //import { CreateDocumentDto } from '../dto/create-document.dto'; +// import { UpdateDocumentDto } from '../dto/update-document.dto'; +// import { AdminGuard } from '../auth/admin.guard'; +// import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +// import { CreateUserDto } from '../dto/create-user.dto'; +// import { S3Service } from 'src/s3/s3.service'; +// import { PrismaService } from 'src/prisma/prisma.service'; + +// @Controller('admin') +// @UseGuards(JwtAuthGuard, AdminGuard) +// export class AdminController { +// constructor( +// private readonly adminService: AdminService, +// private readonly s3Service: S3Service, +// private readonly prisma: PrismaService, +// ) {} + +// @Get('documents') +// getAllDocuments() { +// return this.adminService.getAllDocuments(); +// } + +// @Put('documents/:id') +// @UseInterceptors(FileInterceptor('file')) +// updateDocument( +// @Param('id', ParseIntPipe) id: number, +// @Body() updateDocumentDto: UpdateDocumentDto, +// @UploadedFile() file?: Express.Multer.File, +// ) { +// return this.adminService.updateDocument(id, updateDocumentDto, file); +// } + +// @Get('users') +// getAllUsers() { +// return this.adminService.getAllUsers(); +// } +// @Post('test-document') +// async testDocumentCreation() { +// try { +// const document = await this.prisma.document.create({ +// data: { +// title: 'Test Document', +// s3Key: 'test-key', +// status: 'completed', +// sharedWithId: 2, // ID of 'pero' user +// }, +// }); +// return document; +// } catch (error) { +// console.error('Test document creation error:', error); +// throw error; +// } +// } + +// @Post('users') +// async createUser(@Body() createUserDto: CreateUserDto) { +// return this.adminService.createUser(createUserDto); +// } + +// @Post('documents/:id/share') +// async shareDocument( +// @Param('id') id: string, +// @Body() { userId }: { userId: number }, +// ) { +// return this.adminService.shareDocument(+id, userId); +// } + +// @Put('documents/:id/status') +// async updateDocumentStatus( +// @Param('id') id: string, +// @Body() { status }: { status: string }, +// ) { +// return this.adminService.updateDocumentStatus(+id, status); +// } + +// @Post('documents') +// @UseInterceptors(FileInterceptor('file')) +// async uploadDocument( +// @UploadedFile() file: Express.Multer.File, +// @Body('title') title: string, +// @Body('sharedWithId') sharedWithId: number, +// @Body('uploadedById') uploadedById: number +// ) { +// const document = await this.adminService.uploadDocument( +// file, +// title, +// sharedWithId, +// uploadedById // Add this missing parameter +// ); +// return document; +// } +// @Get('test-s3-connection') +// async testS3Connection() { +// const isConnected = await this.s3Service.testConnection(); +// if (isConnected) { +// return { message: 'Successfully connected to S3' }; +// } else { +// return { message: 'Failed to connect to S3' }; +// } +// } +// } import { Controller, Get, @@ -9,10 +124,10 @@ import { UploadedFile, ParseIntPipe, UseGuards, + BadRequestException, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { AdminService } from './admin.service'; -//import { CreateDocumentDto } from '../dto/create-document.dto'; import { UpdateDocumentDto } from '../dto/update-document.dto'; import { AdminGuard } from '../auth/admin.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; @@ -44,27 +159,60 @@ export class AdminController { return this.adminService.updateDocument(id, updateDocumentDto, file); } + @Post('documents') +@UseInterceptors(FileInterceptor('file')) +async uploadDocument( + @UploadedFile() file: Express.Multer.File, + @Body('title') title: string, + @Body('sharedWithId') sharedWithId: string, // Accept as string first + @Body('uploadedById') uploadedById: string // Accept as string first +) { + if (!sharedWithId || !uploadedById) { + throw new BadRequestException('sharedWithId and uploadedById are required'); + } + + // Parse the string values to numbers + const parsedSharedWithId = parseInt(sharedWithId, 10); + const parsedUploadedById = parseInt(uploadedById, 10); + + // Validate that the parsing was successful + if (isNaN(parsedSharedWithId) || isNaN(parsedUploadedById)) { + throw new BadRequestException('sharedWithId and uploadedById must be valid numbers'); + } + + const document = await this.adminService.uploadDocument( + file, + title, + parsedSharedWithId, + parsedUploadedById + ); + return document; + } + @Get('users') getAllUsers() { return this.adminService.getAllUsers(); } - @Post('test-document') - async testDocumentCreation() { - try { - const document = await this.prisma.document.create({ - data: { - title: 'Test Document', - s3Key: 'test-key', - status: 'completed', - sharedWithId: 2, // ID of 'pero' user - }, - }); - return document; - } catch (error) { - console.error('Test document creation error:', error); - throw error; - } - } + + // @Post('test-document') + // async testDocumentCreation() { + // try { + // const document = await this.prisma.document.create({ + // data: { + // title: 'Test Document', + // s3Key: 'test-key', + // status: 'completed', + // sharedWith: { + // connect: { id: 2 } // ID of 'pero' user + // } + // }, + // }); + // return document; + // } catch (error) { + // console.error('Test document creation error:', error); + // throw error; + // } + // } @Post('users') async createUser(@Body() createUserDto: CreateUserDto) { @@ -87,41 +235,14 @@ export class AdminController { return this.adminService.updateDocumentStatus(+id, status); } - @Post('documents') - @UseInterceptors(FileInterceptor('file')) - async uploadDocument( - @UploadedFile() file: Express.Multer.File, - @Body('title') title: string, - @Body('sharedWith') sharedWithId: string, - ) { - console.log('Received upload request:', { - fileExists: !!file, - fileSize: file?.size, - title, - sharedWithId, - }); - try { - const document = await this.adminService.uploadDocument( - file, - title, - Number(sharedWithId), - ); - - console.log('Document created:', document); - return document; - } catch (error) { - console.error('Upload error:', error); - throw error; - } - } - @Get('test-s3-connection') - async testS3Connection() { - const isConnected = await this.s3Service.testConnection(); - if (isConnected) { - return { message: 'Successfully connected to S3' }; - } else { - return { message: 'Failed to connect to S3' }; - } - } -} + // @Get('test-s3-connection') + // async testS3Connection() { + // const isConnected = await this.s3Service.testConnection(); + // if (isConnected) { + // return { message: 'Successfully connected to S3' }; + // } else { + // return { message: 'Failed to connect to S3' }; + // } + // } +} \ No newline at end of file diff --git a/backend/imk-backend/src/admin/admin.service.ts b/backend/imk-backend/src/admin/admin.service.ts index 38d5ceb..fce6114 100644 --- a/backend/imk-backend/src/admin/admin.service.ts +++ b/backend/imk-backend/src/admin/admin.service.ts @@ -1,7 +1,6 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { S3Service } from '../s3/s3.service'; -//import { CreateDocumentDto } from '../dto/create-document.dto'; import { UpdateDocumentDto } from '../dto/update-document.dto'; import { CreateUserDto } from '../dto/create-user.dto'; import * as bcrypt from 'bcrypt'; @@ -9,93 +8,44 @@ import * as bcrypt from 'bcrypt'; @Injectable() export class AdminService { constructor( - private prisma: PrismaService, - private s3Service: S3Service, + private readonly prisma: PrismaService, + private readonly s3Service: S3Service, ) {} - async uploadDocument( - file: Express.Multer.File, - title: string, - sharedWithId: number, - ) { - try { - // First verify the user exists - console.log('Verifying user:', sharedWithId); - const user = await this.prisma.user.findUnique({ - where: { id: sharedWithId }, - }); - - if (!user) { - throw new NotFoundException(`User with ID ${sharedWithId} not found`); - } - - console.log('Found user:', user); - - // Upload file to S3 - console.log('Uploading file to S3...'); - const s3Key = await this.s3Service.uploadFile(file, 'documents'); - console.log('File uploaded to S3:', s3Key); - - console.log('Creating document record with data:', { - title, - s3Key, - sharedWithId, - }); - - const document = await this.prisma.document.create({ - data: { - title, - s3Key, - status: 'completed', - sharedWithId, - }, - include: { - sharedWith: { - select: { - id: true, - name: true, - email: true, - }, - }, - }, - }); - - console.log('Created document:', document); - return document; - } catch (error) { - console.error('Error in uploadDocument:', error); - if (error.code === 'P2002') { - console.error('Unique constraint violation'); - } - if (error.code === 'P2003') { - console.error('Foreign key constraint violation'); - } - throw error; - } - } async getAllDocuments() { - try { - const documents = await this.prisma.document.findMany({ - include: { - sharedWith: { - select: { - id: true, - name: true, - email: true, - }, + return this.prisma.document.findMany({ + include: { + uploadedBy: { + select: { + id: true, + name: true, + email: true, }, }, - orderBy: { - createdAt: 'desc', + sharedWith: { + select: { + id: true, + name: true, + email: true, + }, }, - }); + }, + }); + } - console.log('Retrieved documents with shared users:', documents); - return documents; - } catch (error) { - console.error('Error fetching documents:', error); - throw error; - } + async getAllUsers() { + return this.prisma.user.findMany(); + } + + async createUser(createUserDto: CreateUserDto) { + const hashedPassword = await bcrypt.hash(createUserDto.password, 10); + + return this.prisma.user.create({ + data: { + ...createUserDto, + password: hashedPassword, + }, + }); } async updateDocument( @@ -103,76 +53,26 @@ export class AdminService { updateDocumentDto: UpdateDocumentDto, file?: Express.Multer.File, ) { - const { ...documentData } = updateDocumentDto; - let s3Key: string | undefined; - + let s3Key = undefined; if (file) { - const oldDocument = await this.prisma.document.findUnique({ - where: { id }, - select: { s3Key: true }, - }); - if (oldDocument?.s3Key) { - await this.s3Service.deleteFile(oldDocument.s3Key); - } s3Key = await this.s3Service.uploadFile(file, 'documents'); } return this.prisma.document.update({ where: { id }, data: { - ...documentData, + ...updateDocumentDto, ...(s3Key && { s3Key }), }, }); } - // async createUser(createUserDto: CreateUserDto) { - // const hashedPassword = await bcrypt.hash(createUserDto.password, 10); - // return this.prisma.user.create({ - // data: { - // ...createUserDto, - // password: hashedPassword, - // }, - // }); - // } - - async createUser(userData: { - name: string; - email: string; - password: string; - isAdmin: boolean; - }) { - const hashedPassword = await bcrypt.hash(userData.password, 10); - - return this.prisma.user.create({ - data: { - name: userData.name, - email: userData.email, - password: hashedPassword, - isAdmin: userData.isAdmin, - }, - select: { - id: true, - name: true, - email: true, - isAdmin: true, - }, - }); - } - async shareDocument(documentId: number, userId: number) { return this.prisma.document.update({ where: { id: documentId }, data: { - sharedWithId: userId, - }, - include: { sharedWith: { - select: { - id: true, - name: true, - email: true, - }, + connect: { id: userId }, }, }, }); @@ -185,43 +85,42 @@ export class AdminService { }); } - async getDocumentUrl(documentId: number) { - const document = await this.prisma.document.findUnique({ - where: { id: documentId }, - }); - if (!document) { - throw new NotFoundException('Document not found'); - } - return this.s3Service.getFileUrl(document.s3Key); - } - async getUserWithDocuments(userId: number) { - return this.prisma.user.findUnique({ - where: { id: userId }, - select: { - id: true, - name: true, - email: true, - sharedDocuments: true, + async uploadDocument( + file: Express.Multer.File, + title: string, + sharedWithId: number, + uploadedById: number + ) { + const s3Key = await this.s3Service.uploadFile(file, 'documents'); + + return this.prisma.document.create({ + data: { + title, + s3Key, + status: 'pending', + sharedWith: { + connect: { id: sharedWithId } + }, + uploadedBy: { + connect: { id: uploadedById } + } + }, + include: { + uploadedBy: { + select: { + id: true, + name: true, + email: true, + }, + }, + sharedWith: { + select: { + id: true, + name: true, + email: true, + }, + }, }, }); } - - async getAllUsers() { - try { - const users = await this.prisma.user.findMany({ - select: { - id: true, - name: true, - email: true, - isAdmin: true, - sharedDocuments: true, - }, - }); - console.log('All users:', users); - return users; - } catch (error) { - console.error('Error fetching users:', error); - throw error; - } - } -} +} \ No newline at end of file diff --git a/backend/imk-backend/src/app.module.ts b/backend/imk-backend/src/app.module.ts index 25ace33..bdb51f4 100644 --- a/backend/imk-backend/src/app.module.ts +++ b/backend/imk-backend/src/app.module.ts @@ -14,6 +14,7 @@ import { PrismaModule } from './prisma/prisma.module'; import { ConfigModule } from '@nestjs/config'; import { AuthController } from './auth/auth.controller'; import { DocumentsController } from './documents/documents.controller'; +import { JwtModule } from '@nestjs/jwt'; @Module({ imports: [ @@ -26,7 +27,13 @@ import { DocumentsController } from './documents/documents.controller'; // database: 'imk', // synchronize: true, // }), - ConfigModule.forRoot(), + ConfigModule.forRoot({ + isGlobal: true, + }), + JwtModule.register({ + secret: process.env.JWT_SECRET, + signOptions: { expiresIn: '1h' }, + }), AuthModule, AdminModule, ClientModule, diff --git a/backend/imk-backend/src/auth/auth.service.ts b/backend/imk-backend/src/auth/auth.service.ts index 147706d..ac68869 100644 --- a/backend/imk-backend/src/auth/auth.service.ts +++ b/backend/imk-backend/src/auth/auth.service.ts @@ -3,12 +3,14 @@ import { JwtService } from '@nestjs/jwt'; import { PrismaService } from '../prisma/prisma.service'; import * as bcrypt from 'bcrypt'; import { CreateUserDto } from '../dto/create-user.dto'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class AuthService { constructor( private prisma: PrismaService, private jwtService: JwtService, + private configService: ConfigService, ) {} async validateUser(username: string, password: string): Promise { @@ -24,9 +26,16 @@ export class AuthService { } async login(user: any) { - const payload = { username: user.email, sub: user.id }; + // const payload = { username: user.email, sub: user.id }; + // return { + // access_token: this.jwtService.sign(payload), + // }; + const payload = { username: user.username, sub: user.id }; + console.log(payload); return { - access_token: this.jwtService.sign(payload), + access_token: this.jwtService.sign(payload, { + secret: this.configService.get('JWT_SECRET'), + }), }; } @@ -59,15 +68,32 @@ export class AuthService { return result; } + // async getUserInfo(userId: number) { + // return this.prisma.user.findUnique({ + // where: { id: userId }, + // select: { + // id: true, + // name: true, + // email: true, + // isAdmin: true, + // }, + // }); + // } async getUserInfo(userId: number) { + if (!userId) { + throw new Error('User ID is required'); + } + return this.prisma.user.findUnique({ - where: { id: userId }, + where: { + id: userId, // Make sure userId is properly passed and converted to number if needed + }, select: { id: true, name: true, email: true, - isAdmin: true, - }, + isAdmin: true + } }); } } diff --git a/backend/imk-backend/src/auth/jwt.strategy.ts b/backend/imk-backend/src/auth/jwt.strategy.ts index 10a008d..b166ae8 100644 --- a/backend/imk-backend/src/auth/jwt.strategy.ts +++ b/backend/imk-backend/src/auth/jwt.strategy.ts @@ -1,15 +1,15 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable } from '@nestjs/common'; -import { jwtConstants } from './constants'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor() { + constructor(configService: ConfigService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: jwtConstants.secret, + secretOrKey: configService.get('JWT_SECRET'), }); } diff --git a/backend/imk-backend/src/client/client.service.ts b/backend/imk-backend/src/client/client.service.ts index 16bee51..62e1c1e 100644 --- a/backend/imk-backend/src/client/client.service.ts +++ b/backend/imk-backend/src/client/client.service.ts @@ -15,7 +15,11 @@ export class ClientService { async getDocuments(userId: string) { return this.prisma.document.findMany({ where: { - sharedWithId: Number(userId), + sharedWith: { + some: { + id: Number(userId), + }, + }, }, include: { sharedWith: { diff --git a/backend/imk-backend/src/documents/documents.controller.ts b/backend/imk-backend/src/documents/documents.controller.ts index 200f08b..3895f25 100644 --- a/backend/imk-backend/src/documents/documents.controller.ts +++ b/backend/imk-backend/src/documents/documents.controller.ts @@ -1,33 +1,81 @@ - -import { Controller, Get, Param, Req, Res, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, Req, Res, UseGuards, Logger, Request } from '@nestjs/common'; import { Response } from 'express'; import { DocumentsService } from './documents.service'; -import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { S3Service } from '../s3/s3.service'; + +interface S3File { + buffer: Buffer; + contentType: string; + contentLength: number; + fileName: string; +} @Controller('documents') export class DocumentsController { - constructor(private readonly documentsService: DocumentsService) {} + private readonly logger = new Logger(DocumentsController.name); + + constructor( + private readonly documentsService: DocumentsService, + private readonly s3Service: S3Service + ) {} @Get('shared/:userId') + @UseGuards(JwtAuthGuard) async getSharedDocuments(@Param('userId') userId: string) { - return this.documentsService.getClientDocuments(parseInt(userId)); + return this.documentsService.getClientDocuments(Number(userId)); } - @Get('shared/download/:key') + @Get('download/:key') @UseGuards(JwtAuthGuard) async downloadDocument( @Param('key') key: string, - @Req() req, + @Request() 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}"`, - }); + try { + this.logger.debug(`Download request for key: ${key}`); + + const decodedKey = decodeURIComponent(key); + this.logger.debug(`Decoded key: ${decodedKey}`); - res.send(file.buffer); + // Get document from database first to verify access + const document = await this.documentsService.findDocumentByS3Key(decodedKey); + + if (!document) { + return res.status(404).json({ message: 'Document not found' }); + } + + // Verify user has access to this document + const hasAccess = await this.documentsService.verifyDocumentAccess( + document.id, + req.user.id + ); + + if (!hasAccess) { + return res.status(403).json({ message: 'Access denied' }); + } + + // Get the file from S3 + const file = await this.s3Service.getFile(decodedKey); + + if (!file || !file.buffer) { + return res.status(404).json({ message: 'File not found in storage' }); + } + + res.set({ + 'Content-Type': file.contentType || 'application/octet-stream', + 'Content-Length': file.contentLength, + 'Content-Disposition': `attachment; filename="${encodeURIComponent(file.fileName)}"`, + }); + + return res.send(file.buffer); + } catch (error) { + this.logger.error('Download error:', error); + return res.status(500).json({ + message: 'Failed to download file', + error: error.message + }); + } } -} \ No newline at end of file +} diff --git a/backend/imk-backend/src/documents/documents.service.ts b/backend/imk-backend/src/documents/documents.service.ts index ddb69c8..48b9a3d 100644 --- a/backend/imk-backend/src/documents/documents.service.ts +++ b/backend/imk-backend/src/documents/documents.service.ts @@ -1,236 +1,120 @@ -// // 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 { S3Service } from '../s3/s3.service'; -// import { Document, User } from '@prisma/client'; - -// @Injectable() -// export class DocumentsService { -// constructor( -// private prisma: PrismaService, -// private s3Service: S3Service, -// ) {} - -// async findAllByClient(userId: number): Promise { -// const documents = await this.prisma.document.findMany({ -// where: { -// 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 { -// // 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 { -// return this.prisma.document.findMany({ -// include: { -// sharedWith: { -// select: { -// id: true, -// name: true, -// email: true, -// }, -// }, -// }, -// orderBy: { -// createdAt: 'desc', -// }, -// }); -// } - -// async shareDocument(documentId: number, userId: number): Promise { -// 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 { -// 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, Logger, NotFoundException, ForbiddenException } 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'; +import { Document, User, Prisma } from '@prisma/client'; @Injectable() export class DocumentsService { + private readonly logger = new Logger(DocumentsService.name); + constructor( private readonly prisma: PrismaService, - private readonly s3Service: S3Service, + private readonly s3Service: S3Service ) {} - async getClientDocuments(clientId: number) { - return this.prisma.document.findMany({ - where: { - sharedWithId: clientId, - }, - orderBy: { - createdAt: 'desc', + async findDocumentByS3Key(s3Key: string) { + return this.prisma.document.findFirst({ + where: { s3Key }, + include: { + uploadedBy: true, + sharedWith: true, }, }); } - async downloadDocument(s3Key: string, userId: number) { - // Verify document access - const document = await this.prisma.document.findFirst({ - where: { - s3Key: s3Key, - sharedWithId: userId, + async verifyDocumentAccess(documentId: number, userId: number): Promise { + const document = await this.prisma.document.findUnique({ + where: { id: documentId }, + include: { + uploadedBy: true, + sharedWith: { + where: { + id: userId + } + } }, }); if (!document) { - throw new NotFoundException('Document not found or access denied'); + return false; } - try { - 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 from S3:', error); - throw new NotFoundException('Failed to download file'); - } + // User has access if they uploaded the document or it's shared with them + return document.uploadedBy.id === userId || document.sharedWith.length > 0; } -} \ No newline at end of file + + async getClientDocuments(clientId: number) { + return this.prisma.document.findMany({ + where: { + OR: [ + { uploadedById: clientId }, + { + sharedWith: { + some: { + id: clientId + } + } + } + ] + }, + include: { + uploadedBy: { + select: { + id: true, + name: true, + email: true + } + }, + sharedWith: { + select: { + id: true, + name: true, + email: true + } + } + }, + orderBy: { + createdAt: 'desc' + } + }); + } + + async uploadDocument( + file: Express.Multer.File, + title: string, + sharedWithId: number, + uploadedById: number + ) { + const s3Key = await this.s3Service.uploadFile(file, 'documents'); + + return this.prisma.document.create({ + data: { + title, + s3Key, + status: 'pending', + uploadedBy: { + connect: { id: uploadedById } + }, + sharedWith: { + connect: { id: sharedWithId } + } + }, + include: { + uploadedBy: { + select: { + id: true, + name: true, + email: true, + }, + }, + sharedWith: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + }); + } +} diff --git a/backend/imk-backend/src/interfaces/file-response.interface.ts b/backend/imk-backend/src/interfaces/file-response.interface.ts new file mode 100644 index 0000000..700c5cc --- /dev/null +++ b/backend/imk-backend/src/interfaces/file-response.interface.ts @@ -0,0 +1,6 @@ +export interface FileResponse { + buffer: Buffer; + contentType: string; + contentLength: number; + fileName: string; + } \ No newline at end of file diff --git a/backend/imk-backend/src/interfaces/s3-file.interface.ts b/backend/imk-backend/src/interfaces/s3-file.interface.ts new file mode 100644 index 0000000..a681466 --- /dev/null +++ b/backend/imk-backend/src/interfaces/s3-file.interface.ts @@ -0,0 +1,6 @@ +export interface S3File { + buffer: Buffer; + contentType: string; + contentLength: number; + fileName: string; + } \ No newline at end of file diff --git a/backend/imk-backend/src/s3/s3.service.ts b/backend/imk-backend/src/s3/s3.service.ts index 8139317..42e7e80 100644 --- a/backend/imk-backend/src/s3/s3.service.ts +++ b/backend/imk-backend/src/s3/s3.service.ts @@ -1,101 +1,70 @@ - -import { Injectable, Logger } from '@nestjs/common'; -import { - S3Client, - DeleteObjectCommand, - GetObjectCommand, - ListObjectsCommand, - PutObjectCommand, -} from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { Upload } from '@aws-sdk/lib-storage'; +import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; +import { S3File } from '../interfaces/s3-file.interface'; @Injectable() export class S3Service { - private s3Client: S3Client; + private readonly s3Client: S3Client; private readonly logger = new Logger(S3Service.name); - constructor(private configService: ConfigService) { + constructor(private readonly configService: ConfigService) { this.s3Client = new S3Client({ - region: this.configService.get('AWS_REGION'), - endpoint: this.configService.get('AWS_ENDPOINT_URL'), + region: this.configService.get('AWS_REGION'), credentials: { - accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'), - secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'), + accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'), + secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'), }, - forcePathStyle: true, // Needed for non-AWS S3 compatible services + endpoint: this.configService.get('AWS_ENDPOINT_URL'), + forcePathStyle: true, // Required for Contabo Object Storage }); } - async getObject(key: string) { - try { - const command = new GetObjectCommand({ - Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), - Key: key, - }); - - const response = await this.s3Client.send(command); - return response; - } catch (error) { - this.logger.error(`Error getting object from S3: ${error.message}`); - throw error; - } - } async uploadFile(file: Express.Multer.File, folder: string): Promise { try { const key = `${folder}/${Date.now()}-${file.originalname}`; const command = new PutObjectCommand({ - Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), + Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), Key: key, Body: file.buffer, ContentType: file.mimetype, }); await this.s3Client.send(command); + this.logger.debug(`File uploaded successfully: ${key}`); + return key; } catch (error) { this.logger.error(`Error uploading file to S3: ${error.message}`); - throw error; + throw new InternalServerErrorException('Failed to upload file to storage'); } } - async deleteFile(key: string): Promise { + async getFile(key: string): Promise { try { - const command = new DeleteObjectCommand({ - Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), + const command = new GetObjectCommand({ + Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), 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 { - const command = new GetObjectCommand({ - Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), - Key: key, - }); - - const url = await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); // URL expires in 1 hour - return url; - } - - async testConnection(): Promise { - try { - await this.getObject('test-connection'); - return true; - } catch (error) { - if (error.name === 'NoSuchKey') { - // This is expected as we're just testing connection - return true; + const response = await this.s3Client.send(command); + + const chunks = []; + for await (const chunk of response.Body as any) { + chunks.push(chunk); } - this.logger.error('Failed to connect to S3', error); - return false; + const buffer = Buffer.concat(chunks); + + return { + buffer, + contentType: response.ContentType || 'application/octet-stream', + contentLength: response.ContentLength || buffer.length, + fileName: key.split('/').pop() || 'download', + }; + } catch (error) { + this.logger.error(`Error getting file from S3: ${error.message}`); + throw new InternalServerErrorException('Failed to retrieve file from storage'); } } } diff --git a/frontend/imk/src/components/adminPanel/AdminPanel.jsx b/frontend/imk/src/components/adminPanel/AdminPanel.jsx index 6404f15..7052cb3 100644 --- a/frontend/imk/src/components/adminPanel/AdminPanel.jsx +++ b/frontend/imk/src/components/adminPanel/AdminPanel.jsx @@ -271,5 +271,714 @@ function AdminPanel() { ); } +export default AdminPanel; -export default AdminPanel; \ No newline at end of file +// import { useState, useEffect } from 'react'; +// import { useAuth } from '../../hooks/useAuth'; +// import { format } from 'date-fns'; +// import { FiUpload, FiUsers, FiFile, FiTrash2, FiShare2, FiLoader, FiSearch, FiChevronRight } from 'react-icons/fi'; +// import api from '../../services/api'; + +// function AdminPanel() { +// const [documents, setDocuments] = useState([]); +// const [users, setUsers] = useState([]); +// const [loading, setLoading] = useState(true); +// const [error, setError] = useState(''); +// const [selectedFile, setSelectedFile] = useState(null); +// const [uploadTitle, setUploadTitle] = useState(''); +// const [selectedUser, setSelectedUser] = useState(''); +// const [searchTerm, setSearchTerm] = useState(''); +// const [expandedUsers, setExpandedUsers] = useState({}); + +// useEffect(() => { +// fetchData(); +// }, []); + +// const fetchData = async () => { +// try { +// setLoading(true); +// const [documentsRes, usersRes] = await Promise.all([ +// api.get('/admin/documents'), +// api.get('/admin/users') +// ]); + +// console.log('Documents response:', documentsRes.data); +// console.log('Users response:', usersRes.data); + +// // Group documents by user +// const groupedDocs = groupDocumentsByUser(documentsRes.data || []); +// setDocuments(groupedDocs); +// setUsers(usersRes.data || []); +// } catch (err) { +// setError('Failed to fetch data'); +// console.error('Error:', err); +// } finally { +// setLoading(false); +// } +// }; +// const groupDocumentsByUser = (docs) => { +// return docs.reduce((acc, doc) => { +// const userName = doc.sharedWith?.name || 'Unassigned'; +// if (!acc[userName]) { +// acc[userName] = []; +// } +// acc[userName].push(doc); +// return acc; +// }, {}); +// }; + +// const toggleUser = (userName) => { +// setExpandedUsers(prev => ({ +// ...prev, +// [userName]: !prev[userName] +// })); +// }; + +// const handleFileUpload = async (e) => { +// e.preventDefault(); +// if (!selectedFile || !uploadTitle || !selectedUser) { +// alert('Please fill in all fields'); +// return; +// } + +// const formData = new FormData(); +// formData.append('file', selectedFile); +// formData.append('title', uploadTitle); +// formData.append('sharedWith', selectedUser); + +// try { +// await api.post('/admin/documents', formData, { +// headers: { +// 'Content-Type': 'multipart/form-data', +// }, +// }); + +// setSelectedFile(null); +// setUploadTitle(''); +// setSelectedUser(''); +// fetchData(); +// alert('Document uploaded successfully'); +// } catch (err) { +// console.error('Upload error:', err); +// alert('Failed to upload document'); +// } +// }; + +// const handleDelete = async (documentId) => { +// if (!window.confirm('Are you sure you want to delete this document?')) { +// return; +// } + +// try { +// await api.delete(`/admin/documents/${documentId}`); +// fetchData(); +// alert('Document deleted successfully'); +// } catch (err) { +// console.error('Delete error:', err); +// alert('Failed to delete document'); +// } +// }; + +// const handleShare = async (documentId) => { +// const userId = prompt('Enter user ID to share with:'); +// if (!userId) return; + +// try { +// await api.post(`/admin/documents/${documentId}/share`, { userId }); +// fetchData(); +// alert('Document shared successfully'); +// } catch (err) { +// console.error('Share error:', err); +// alert('Failed to share document'); +// } +// }; + +// if (loading) { +// return ( +//
+//
+// +// Loading admin panel... +//
+//
+// ); +// } + +// if (error) { +// return ( +//
+//
+// {error} +//
+//
+// ); +// } + +// return ( +//
+//
+//
+//

Admin Dashboard

+//

Manage documents and users

+//
+ +// {/* Upload Section - remains the same */} +//
+// {/* ... upload form content remains the same ... */} +//
+ +// {/* Search Bar */} +//
+// +// setSearchTerm(e.target.value)} +// className="w-full pl-10 pr-4 py-3 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400" +// /> +//
+ +// {/* Documents List - Grouped by User */} +//
+// {Object.entries(documents).map(([userName, userDocs]) => { +// const filteredDocs = userDocs.filter(doc => +// doc.title.toLowerCase().includes(searchTerm.toLowerCase()) +// ); + +// if (filteredDocs.length === 0) return null; + +// return ( +//
+// + +// {expandedUsers[userName] && ( +//
+// {filteredDocs.map(doc => ( +//
+//
+// +//
+//

{doc.title}

+//

+// Added: {format(new Date(doc.createdAt), 'MMM dd, yyyy')} +//

+//
+//
+ +//
+// +// +//
+//
+// ))} +//
+// )} +//
+// ); +// })} + +// {Object.keys(documents).length === 0 && ( +//
+// No documents available +//
+// )} +//
+//
+//
+// ) + +// } + +// export default AdminPanel; +// import { useState, useEffect } from 'react'; +// import { useAuth } from '../../hooks/useAuth'; +// import { format } from 'date-fns'; +// import { +// FiUpload, +// FiDownload, +// FiUsers, +// FiFile, +// FiTrash2, +// FiShare2, +// FiLoader, +// FiSearch, +// FiChevronRight, +// FiUserPlus, +// FiEdit2, +// FiKey +// } from 'react-icons/fi'; +// import api from '../../services/api'; + +// function AdminPanel() { +// const [documents, setDocuments] = useState({}); +// const [users, setUsers] = useState([]); +// const [expandedUsers, setExpandedUsers] = useState({}); +// const [loading, setLoading] = useState(true); +// const [error, setError] = useState(''); +// const [selectedFile, setSelectedFile] = useState(null); +// const [uploadTitle, setUploadTitle] = useState(''); +// const [selectedUser, setSelectedUser] = useState(''); +// const [searchTerm, setSearchTerm] = useState(''); +// const [activeTab, setActiveTab] = useState('documents'); // 'documents' or 'users' +// const [newUser, setNewUser] = useState({ username: '', password: '', name: '', isAdmin: false }); + +// useEffect(() => { +// fetchData(); +// }, []); + +// const fetchData = async () => { +// try { +// setLoading(true); +// const [documentsRes, usersRes] = await Promise.all([ +// api.get('/admin/documents'), +// api.get('/admin/users') +// ]); + +// const groupedDocs = groupDocumentsByUser(documentsRes.data || []); +// const initialExpandedState = Object.keys(groupedDocs).reduce((acc, userName) => { +// acc[userName] = false; +// return acc; +// }, {}); + +// setDocuments(groupedDocs); +// setUsers(usersRes.data || []); +// setExpandedUsers(initialExpandedState); +// } catch (err) { +// setError('Failed to fetch data'); +// console.error('Error:', err); +// } finally { +// setLoading(false); +// } +// }; + +// const groupDocumentsByUser = (docs) => { +// return docs.reduce((acc, doc) => { +// const userName = doc.sharedWith?.name || 'Unassigned'; +// if (!acc[userName]) { +// acc[userName] = []; +// } +// acc[userName].push(doc); +// return acc; +// }, {}); +// }; + +// const handleAddUser = async (e) => { +// e.preventDefault(); +// try { +// await api.post('/admin/users', newUser); +// setNewUser({ username: '', password: '', name: '', isAdmin: false }); +// fetchData(); +// alert('User added successfully'); +// } catch (err) { +// console.error('Error adding user:', err); +// alert('Failed to add user'); +// } +// }; + +// const handleDeleteUser = async (userId) => { +// if (!window.confirm('Are you sure you want to delete this user?')) return; +// try { +// await api.delete(`/admin/users/${userId}`); +// fetchData(); +// alert('User deleted successfully'); +// } catch (err) { +// console.error('Error deleting user:', err); +// alert('Failed to delete user'); +// } +// }; + +// const handleResetPassword = async (userId) => { +// const newPassword = prompt('Enter new password:'); +// if (!newPassword) return; + +// try { +// await api.post(`/admin/users/${userId}/reset-password`, { password: newPassword }); +// alert('Password reset successfully'); +// } catch (err) { +// console.error('Error resetting password:', err); +// alert('Failed to reset password'); +// } +// }; + +// const handleFileUpload = async (e) => { +// e.preventDefault(); +// if (!selectedFile || !uploadTitle || !selectedUser) { +// alert('Please fill in all fields'); +// return; +// } + +// const formData = new FormData(); +// formData.append('file', selectedFile); +// formData.append('title', uploadTitle); +// formData.append('sharedWith', selectedUser); + +// try { +// await api.post('/admin/documents', formData, { +// headers: { +// 'Content-Type': 'multipart/form-data', +// }, +// }); + +// setSelectedFile(null); +// setUploadTitle(''); +// setSelectedUser(''); +// fetchData(); +// alert('Document uploaded successfully'); +// } catch (err) { +// console.error('Upload error:', err); +// alert('Failed to upload document'); +// } +// }; +// const toggleUser = (userName) => { +// console.log('Toggling user:', userName); // Debug log +// setExpandedUsers(prev => ({ +// ...prev, +// [userName]: !prev[userName] +// })); +// }; + + + +// const handleDownload = async (s3Key, fileName) => { +// try { +// const token = localStorage.getItem('token'); +// // Remove 'documents/' from the s3Key as it's already in the path +// const cleanS3Key = s3Key.replace('documents/', ''); + +// const response = await fetch(`http://localhost:3000/admin/documents/download/${cleanS3Key}`, { +// headers: { +// 'Authorization': `Bearer ${token}` +// } +// }); + +// if (!response.ok) { +// throw new Error('Download failed'); +// } + +// const blob = await response.blob(); +// const url = window.URL.createObjectURL(blob); +// const link = document.createElement('a'); +// link.href = url; +// link.download = fileName; +// document.body.appendChild(link); +// link.click(); +// document.body.removeChild(link); +// window.URL.revokeObjectURL(url); +// } catch (err) { +// console.error('Error downloading document:', err); +// alert('Failed to download document. Please try again.'); +// } +// }; + + +// if (loading) { +// return ( +//
+//
+// +// Loading admin panel... +//
+//
+// ); +// } + +// return ( +//
+//
+//
+//

Admin Dashboard

+//

Manage documents and users

+//
+ +// {/* Tab Navigation */} +//
+// +// +//
+ +// {activeTab === 'documents' ? ( +// <> +// {/* Upload Section */} +//
+//

Upload Document

+//
+//
+//
+// +// setUploadTitle(e.target.value)} +// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400" +// placeholder="Enter document title" +// /> +//
+ +//
+// +// +//
+ +//
+// +// setSelectedFile(e.target.files[0])} +// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-500 file:text-white hover:file:bg-blue-600" +// /> +//
+//
+ +// +//
+//
+ +// {/* Documents List */} +//
+// {Object.entries(documents).map(([userName, userDocs]) => ( +//
+// +// {expandedUsers[userName] && ( +//
+// {userDocs.map(doc => ( +//
+//
+// +//
+//

{doc.title}

+//

+// Added: {format(new Date(doc.createdAt), 'MMM dd, yyyy')} +//

+//
+//
+ +//
+// +// +// +//
+//
+// ))} +//
+// )} +//
+// ))} +//
+// +// ) : ( +// // Users Management Section +//
+// {/* Add New User Form */} +//
+//

Add New User

+//
+//
+// +// setNewUser({ ...newUser, username: e.target.value })} +// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white" +// required +// /> +//
+//
+// +// setNewUser({ ...newUser, password: e.target.value })} +// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white" +// required +// /> +//
+//
+// +// setNewUser({ ...newUser, name: e.target.value })} +// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white" +// required +// /> +//
+//
+// setNewUser({ ...newUser, isAdmin: e.target.checked })} +// className="w-4 h-4 rounded border-gray-700 text-blue-600 focus:ring-blue-500" +// /> +// +//
+//
+// +//
+//
+//
+ +// {/* Users List */} +//
+//
+//

Users

+//
+//
+// {Array.isArray(users) && users.map(user => ( +//
+//
+// +//
+//

{user.name}

+//

@{user.username}

+//
+// {user.isAdmin && ( +// +// Admin +// +// )} +//
+ +//
+// +// +//
+//
+// ))} +//
+//
+//
+// )} +//
+//
+// ); +// } + +// export default AdminPanel; \ No newline at end of file diff --git a/frontend/imk/src/components/dashboard/Dashboard.jsx b/frontend/imk/src/components/dashboard/Dashboard.jsx index c8eed40..515268b 100644 --- a/frontend/imk/src/components/dashboard/Dashboard.jsx +++ b/frontend/imk/src/components/dashboard/Dashboard.jsx @@ -1,13 +1,12 @@ import { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; -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'; +import { format } from 'date-fns'; +import { FiFolder, FiFile, FiDownload, FiChevronRight, FiLoader } from 'react-icons/fi'; +import { downloadDocument } from '../../services/api'; function Dashboard() { - const [documents, setDocuments] = useState([]); + const [documents, setDocuments] = useState({}); const [expandedFolders, setExpandedFolders] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -16,23 +15,29 @@ function Dashboard() { useEffect(() => { const fetchDocuments = async () => { try { + if (!user?.id) return; + const response = await getSharedDocuments(user.id); + // Group the documents by company and date const groupedDocs = groupDocumentsByCompanyAndDate(response.data); setDocuments(groupedDocs); } catch (err) { - setError('Failed to fetch documents'); console.error('Error fetching documents:', err); + setError('Failed to fetch documents'); } finally { setLoading(false); } }; - if (user) { - fetchDocuments(); - } + fetchDocuments(); }, [user]); const groupDocumentsByCompanyAndDate = (docs) => { + if (!Array.isArray(docs)) { + console.error('Expected array of documents, received:', docs); + return {}; + } + return docs.reduce((acc, doc) => { const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`; if (!acc[folderName]) { @@ -43,52 +48,16 @@ function Dashboard() { }, {}); }; -// 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 handleDownload = async (s3Key, fileName) => { + try { + await downloadDocument(s3Key); + } 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 toggleFolder = (folderName) => { + const toggleFolder = (folderName) => { setExpandedFolders(prev => ({ ...prev, [folderName]: !prev[folderName] @@ -97,94 +66,87 @@ const toggleFolder = (folderName) => { if (loading) { return ( -
-
+
+
+ + Loading documents... +
); } if (error) { return ( -
- {error} +
+
+ {error} +
); } return ( -
-

Your Laboratory Reports

- - {Object.entries(documents).length === 0 ? ( -

No documents have been shared with you yet.

- ) : ( -
- {Object.entries(documents).map(([folderName, folderDocs]) => ( - - +
+
+
+

Your Documents

+

Access and manage your shared documents

+
- {expandedFolders[folderName] && ( - + {Object.entries(documents).length > 0 ? ( + Object.entries(documents).map(([folderName, docs]) => ( +
+ + + {expandedFolders[folderName] && ( +
+ {docs.map((doc) => ( +
+
+ + {doc.title} +
+
-
- ))} - - )} - - ))} + ))} +
+ )} +
+ )) + ) : ( +
+

No documents available

+
+ )}
- )} +
); } diff --git a/frontend/imk/src/components/documentUpload/DocumentUpload.jsx b/frontend/imk/src/components/documentUpload/DocumentUpload.jsx index 44f24a5..032f17a 100644 --- a/frontend/imk/src/components/documentUpload/DocumentUpload.jsx +++ b/frontend/imk/src/components/documentUpload/DocumentUpload.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { uploadDocument, getAllUsers } from '../../services/api'; +import { uploadDocument, getAllUsers, getUserInfo } from '../../services/api'; function DocumentUpload() { const [file, setFile] = useState(null); @@ -8,9 +8,11 @@ function DocumentUpload() { const [availableUsers, setAvailableUsers] = useState([]); const [status, setStatus] = useState('idle'); // idle, uploading, completed, failed const [errorMessage, setErrorMessage] = useState(''); + const [currentUser, setCurrentUser] = useState(null); useEffect(() => { fetchUsers(); + getCurrentUser(); }, []); const fetchUsers = async () => { @@ -22,18 +24,22 @@ function DocumentUpload() { } }; - const handleFileChange = (event) => { - const selectedFile = event.target.files[0]; - setFile(selectedFile); + const getCurrentUser = async () => { + try { + const response = await getUserInfo(); + console.log('Current user data:', response.data); // Debug log + setCurrentUser(response.data); + } catch (error) { + console.error('Failed to get current user:', error); + setErrorMessage('Failed to get current user info'); + } }; -const handleSubmit = async (event) => { + const handleSubmit = async (event) => { event.preventDefault(); - console.log('Form submission:', { file, title, selectedUsers }); - - if (!file || !title || selectedUsers.length === 0) { - setErrorMessage('Please provide a title, file, and select at least one user'); + if (!file || !title || selectedUsers.length === 0 || !currentUser) { + setErrorMessage('Please provide all required information'); return; } @@ -43,15 +49,18 @@ const handleSubmit = async (event) => { const formData = new FormData(); formData.append('file', file); formData.append('title', title); - formData.append('sharedWith', selectedUsers[0]); + formData.append('sharedWithId', selectedUsers[0]); // Remove toString() + formData.append('uploadedById', currentUser.id); // Remove toString() + + // Debug log + console.log('Form Data Contents:', { + file: formData.get('file'), + title: formData.get('title'), + sharedWithId: formData.get('sharedWithId'), + uploadedById: formData.get('uploadedById') + }); try { - console.log('Sending request with:', { - title, - sharedWith: selectedUsers[0], - fileSize: file.size - }); - const response = await uploadDocument(formData); console.log('Upload response:', response); @@ -66,6 +75,17 @@ const handleSubmit = async (event) => { setErrorMessage(error.response?.data?.message || 'Upload failed'); } }; + + function handleFileChange(e) { + setFile(e.target.files[0]); + } + + const handleUserSelect = (e) => { + const values = Array.from(e.target.selectedOptions, option => option.value); + console.log('Selected user values:', values); + setSelectedUsers(values); + }; + return (

Upload Document

@@ -103,7 +123,7 @@ const handleSubmit = async (event) => { setUsername(e.target.value)} +// /> +//
+//
+// setPassword(e.target.value)} +// /> +//
+//
+ +//
+// +//
+// +//
+// +// ); +// }; + +// export default Login; import { useState } from 'react'; import { useAuth } from '../../hooks/useAuth'; import { useNavigate } from 'react-router-dom'; +import { FiUser, FiLock } from 'react-icons/fi'; // Import icons const Login = () => { - const [username, setUsername] = useState(''); // Changed from email to username + const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const { login } = useAuth(); @@ -15,8 +96,7 @@ const Login = () => { try { const userData = await login(username, password); - console.log('Login result:', userData); - if (userData?.isAdmin) { + if (userData?.isAdmin) { navigate('/admin'); } else { navigate('/dashboard'); @@ -27,35 +107,39 @@ const Login = () => { }; return ( -
-
-
-

- Sign in to your account -

+
+
+
+

Welcome Back

+

Please sign in to continue

-
+ + {error && ( -
+
{error}
)} -
-
+ +
+
+ setUsername(e.target.value)} />
-
+ +
+ setPassword(e.target.value)} @@ -63,13 +147,17 @@ const Login = () => {
-
- + + +
+

+ Need help? Contact your administrator +

diff --git a/frontend/imk/src/services/api.js b/frontend/imk/src/services/api.js index 1f3a14b..28716b7 100644 --- a/frontend/imk/src/services/api.js +++ b/frontend/imk/src/services/api.js @@ -22,27 +22,50 @@ api.interceptors.request.use( } ); -export const downloadDocument = async (documentId) => { +export const getSharedDocuments = (userId) => { + return api.get(`/documents/shared/${userId}`); +}; + + +const getToken = () => localStorage.getItem('token'); +export const downloadDocument = async (key) => { try { - const response = await api.get(`/documents/download/${documentId}`, { + const response = await api.get(`/documents/download/${encodeURIComponent(key)}`, { responseType: 'blob', headers: { - Authorization: `Bearer ${localStorage.getItem('token')}`, + 'Accept': 'application/octet-stream', } }); - return response.data; + + // Create blob URL and trigger download + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + + // Extract filename from key + const fileName = key.split('/').pop(); + link.download = fileName; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + return response; } catch (error) { console.error('Download error:', error); throw error; } }; + export const createUser = (userData) => { return api.post('/admin/users', { name: userData.name, email: userData.email, password: userData.password, - isAdmin: userData.isAdmin // Add this line + isAdmin: userData.isAdmin }); }; export const login = (username, password) => api.post('/auth/login', { username, password }); @@ -51,15 +74,21 @@ export const updateDocumentStatus = (documentId, status) => api.put(`/admin/docu export const uploadDocument = async (formData) => { try { + // Debug log + console.log('Sending to server:', { + title: formData.get('title'), + sharedWithId: formData.get('sharedWithId'), + uploadedById: formData.get('uploadedById') + }); + const response = await api.post('/admin/documents', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); - console.log('Upload API response:', response.data); return response.data; } catch (error) { - console.error('Upload API error:', error); + console.error('API Error:', error.response?.data); throw error; } }; @@ -67,7 +96,7 @@ export const getUserInfo = () => api.get('/auth/user-info'); export const getAllDocuments = () => api.get('/admin/documents'); -export const getSharedDocuments = (userId) => api.get(`/documents/shared/${userId}`); +// export const getSharedDocuments = (userId) => api.get(`/documents/shared/${userId}`); export const getAllUsers = () => api.get('/admin/users'); export default api;