everything works!

This commit is contained in:
dimitar committed 2024-11-02 09:26:40 +01:00
1 parent 01017bb36e
commit a32992b17b
19 files changed
+1554 -727

No files matched your search

+1
View File
@@ -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
@@ -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;
+24 -20
View File
@@ -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])
}
+176 -55
View File
@@ -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' };
// }
// }
}
+71 -172
View File
@@ -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;
}
}
}
}
+8 -1
View File
@@ -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,
+31 -5
View File
@@ -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<any> {
@@ -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<string>('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
}
});
}
}
+3 -3
View File
@@ -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'),
});
}
@@ -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: {
@@ -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
});
}
}
}
}
@@ -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<Document[]> {
// 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<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, 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<boolean> {
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;
}
}
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,
},
},
},
});
}
}
@@ -0,0 +1,6 @@
export interface FileResponse {
buffer: Buffer;
contentType: string;
contentLength: number;
fileName: string;
}
@@ -0,0 +1,6 @@
export interface S3File {
buffer: Buffer;
contentType: string;
contentLength: number;
fileName: string;
}
+33 -64
View File
@@ -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<string>('AWS_REGION'),
credentials: {
accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
accessKeyId: this.configService.get<string>('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.configService.get<string>('AWS_SECRET_ACCESS_KEY'),
},
forcePathStyle: true, // Needed for non-AWS S3 compatible services
endpoint: this.configService.get<string>('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<string> {
try {
const key = `${folder}/${Date.now()}-${file.originalname}`;
const command = new PutObjectCommand({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
Bucket: this.configService.get<string>('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<void> {
async getFile(key: string): Promise<S3File> {
try {
const command = new DeleteObjectCommand({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
const command = new GetObjectCommand({
Bucket: this.configService.get<string>('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<string> {
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<boolean> {
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');
}
}
}