From e4f6f595501e70ffacb04dd2d3afac5d06cece80 Mon Sep 17 00:00:00 2001 From: dimitar Date: Tue, 25 Feb 2025 20:41:23 +0100 Subject: [PATCH] emailFunc implemeted --- backend/.env | 14 + backend/package-lock.json | 20 ++ backend/package.json | 2 + backend/src/admin/admin.controller.ts | 20 +- backend/src/admin/admin.module.ts | 3 +- backend/src/admin/admin.service.ts | 293 +++++++++++++++--- backend/src/app.module.ts | 2 + backend/src/auth/auth.controller.ts | 129 +++++++- backend/src/auth/auth.module.ts | 27 +- backend/src/auth/auth.service.ts | 210 ++++++++++--- backend/src/auth/jwt.strategy.ts | 50 ++- backend/src/documents/documents.controller.ts | 49 ++- backend/src/documents/documents.module.ts | 14 + backend/src/documents/documents.service.ts | 176 +++++++++-- backend/src/dto/create-user.dto.ts | 2 + backend/src/dto/login.dto.ts | 18 +- backend/src/email/email.module.ts | 10 + backend/src/email/email.service.ts | 187 +++++++++++ backend/src/main.ts | 43 ++- frontend/src/services/api.js | 4 +- 20 files changed, 1110 insertions(+), 163 deletions(-) create mode 100644 backend/src/documents/documents.module.ts create mode 100644 backend/src/email/email.module.ts create mode 100644 backend/src/email/email.service.ts diff --git a/backend/.env b/backend/.env index eaff313..386b54a 100644 --- a/backend/.env +++ b/backend/.env @@ -12,3 +12,17 @@ AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4 AWS_S3_BUCKET_NAME=imk-data AWS_ENDPOINT_URL=https://eu2.contabostorage.com +#Email Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=taratur@gmail.com +SMTP_PASS=dziy nccc svgg bovb +EMAIL_FROM=taratur@gmail.com + +# SMTP_HOST=imk.mk +# SMTP_PORT=465 +# SMTP_USER=mailer@imk.mk +# SMTP_PASSWORD=76Avtostoperski76 +# SMTP_FROM=mailer@imk.mk +# FRONTEND_URL=https://imk.mk +# ADMIN_EMAIL=petrovskidimitar@yandex.com diff --git a/backend/package-lock.json b/backend/package-lock.json index 44f8ca2..f4b3690 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -22,9 +22,11 @@ "@nestjs/typeorm": "^10.0.2", "@prisma/client": "^5.12.1", "@types/multer": "^1.4.12", + "@types/nodemailer": "^6.4.17", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "nodemailer": "^6.10.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", @@ -3860,6 +3862,15 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/passport": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", @@ -8679,6 +8690,15 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, + "node_modules/nodemailer": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 07bc9b8..b58704f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -33,9 +33,11 @@ "@nestjs/typeorm": "^10.0.2", "@prisma/client": "^5.12.1", "@types/multer": "^1.4.12", + "@types/nodemailer": "^6.4.17", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "nodemailer": "^6.10.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts index 9afe69b..b0ece71 100644 --- a/backend/src/admin/admin.controller.ts +++ b/backend/src/admin/admin.controller.ts @@ -80,25 +80,7 @@ async uploadDocument( 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', - // 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) { diff --git a/backend/src/admin/admin.module.ts b/backend/src/admin/admin.module.ts index 271ca20..fc45aa6 100644 --- a/backend/src/admin/admin.module.ts +++ b/backend/src/admin/admin.module.ts @@ -3,10 +3,11 @@ import { AdminController } from './admin.controller'; import { AdminService } from './admin.service'; import { PrismaModule } from '../prisma/prisma.module'; import { S3Module } from '../s3/s3.module'; +import { EmailModule } from '../email/email.module'; @Module({ controllers: [AdminController], providers: [AdminService], - imports: [PrismaModule, S3Module], + imports: [PrismaModule, S3Module, EmailModule], }) export class AdminModule {} diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index 7e29822..98ae256 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -1,15 +1,19 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { S3Service } from '../s3/s3.service'; import { UpdateDocumentDto } from '../dto/update-document.dto'; import { CreateUserDto } from '../dto/create-user.dto'; import * as bcrypt from 'bcrypt'; +import { EmailService } from '../email/email.service'; @Injectable() export class AdminService { + private readonly logger = new Logger(AdminService.name); + constructor( private readonly prisma: PrismaService, private readonly s3Service: S3Service, + private readonly emailService: EmailService, ) {} async getAllDocuments() { @@ -71,14 +75,124 @@ export class AdminService { } async shareDocument(documentId: number, userId: number) { - return this.prisma.document.update({ - where: { id: documentId }, - data: { - sharedWith: { - connect: { id: userId }, + this.logger.log('=== Starting document share process ==='); + console.log('=== Starting document share process ==='); + this.logger.debug('Share request:', { documentId, userId }); + console.log('Share request:', { documentId, userId }); + + try { + // Get the document with its current data + const document = await this.prisma.document.findUnique({ + where: { id: documentId }, + include: { + uploadedBy: { + select: { + id: true, + name: true, + email: true, + }, + }, }, - }, - }); + }); + + if (!document) { + this.logger.error('Document not found:', { documentId }); + console.error('Document not found:', { documentId }); + throw new Error('Document not found'); + } + + // Get the user we're sharing with + const shareUser = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + name: true, + email: true, + }, + }); + + if (!shareUser) { + this.logger.error('User not found:', { userId }); + console.error('User not found:', { userId }); + throw new Error('User not found'); + } + + // Update the document sharing + const updatedDocument = await this.prisma.document.update({ + where: { id: documentId }, + data: { + sharedWith: { + connect: { id: userId }, + }, + }, + include: { + uploadedBy: { + select: { + id: true, + name: true, + email: true, + }, + }, + sharedWith: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + }); + + // Send email notification to the user we're sharing with + this.logger.log('Sending email notification to shared user:', { + userId: shareUser.id, + email: shareUser.email, + name: shareUser.name, + documentTitle: document.title + }); + console.log('Sending email notification to shared user:', { + userId: shareUser.id, + email: shareUser.email, + name: shareUser.name, + documentTitle: document.title + }); + + try { + console.log('Attempting to send email notification...'); + await this.emailService.sendDocumentNotification( + shareUser.email, + shareUser.name, + document.title, + 'shared' + ); + console.log('Email notification sent successfully'); + this.logger.log('Email notification sent successfully'); + } catch (emailError) { + // Log the full error details + console.error('Failed to send email notification:', emailError); + this.logger.error('Failed to send email notification:', { + error: emailError.message, + code: emailError.code, + command: emailError.command, + response: emailError.response, + responseCode: emailError.responseCode, + stack: emailError.stack, + }); + // Don't throw the error, just log it and continue + } + + this.logger.log('=== Document share process completed ==='); + console.log('=== Document share process completed ==='); + return updatedDocument; + } catch (error) { + this.logger.error('Error in shareDocument:', { + error: error.message, + code: error.code, + stack: error.stack, + }); + console.error('Error in shareDocument:', error); + throw error; + } } async updateDocumentStatus(documentId: number, status: string) { @@ -94,36 +208,139 @@ export class AdminService { 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, - }, - }, - }, + this.logger.log('=== Starting document upload process ==='); + this.logger.debug('Upload parameters:', { + title, + sharedWithId, + uploadedById, + fileName: file.originalname, + fileSize: file.size, }); + + try { + this.logger.debug('Uploading file to S3...'); + const s3Key = await this.s3Service.uploadFile(file, 'documents'); + this.logger.debug(`File uploaded to S3 successfully with key: ${s3Key}`); + + this.logger.debug('Creating document record in database...'); + const document = await 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, + }, + }, + }, + }); + this.logger.debug('Document record created:', { + id: document.id, + title: document.title, + uploadedBy: document.uploadedBy, + sharedWith: document.sharedWith, + }); + + // Send email notifications + this.logger.log('=== Starting email notification process ==='); + + // Notify the user who the document is shared with + const sharedUser = document.sharedWith[0]; + if (sharedUser) { + this.logger.debug(`Preparing to send notification to shared user:`, { + id: sharedUser.id, + email: sharedUser.email, + name: sharedUser.name, + }); + try { + this.logger.debug('Calling EmailService.sendDocumentNotification for shared user...'); + await this.emailService.sendDocumentNotification( + sharedUser.email, + sharedUser.name, + document.title, + 'shared' + ); + this.logger.debug('Shared user notification sent successfully'); + } catch (error) { + this.logger.error('Failed to send notification to shared user:', { + error: error.message, + code: error.code, + command: error.command, + stack: error.stack, + }); + // Log the email service instance to verify it's properly injected + this.logger.debug('EmailService instance:', { + exists: !!this.emailService, + type: typeof this.emailService, + methods: Object.keys(Object.getPrototypeOf(this.emailService)), + }); + } + } else { + this.logger.warn('No shared user found in the document record'); + } + + // Notify the uploader + const uploader = document.uploadedBy; + if (uploader) { + this.logger.debug(`Preparing to send notification to uploader:`, { + id: uploader.id, + email: uploader.email, + name: uploader.name, + }); + try { + this.logger.debug('Calling EmailService.sendDocumentNotification for uploader...'); + await this.emailService.sendDocumentNotification( + uploader.email, + uploader.name, + document.title, + 'uploaded' + ); + this.logger.debug('Uploader notification sent successfully'); + } catch (error) { + this.logger.error('Failed to send notification to uploader:', { + error: error.message, + code: error.code, + command: error.command, + stack: error.stack, + }); + // Log the email service instance to verify it's properly injected + this.logger.debug('EmailService instance:', { + exists: !!this.emailService, + type: typeof this.emailService, + methods: Object.keys(Object.getPrototypeOf(this.emailService)), + }); + } + } else { + this.logger.warn('No uploader found in the document record'); + } + + this.logger.log('=== Document upload process completed ==='); + return document; + } catch (error) { + this.logger.error('Error in uploadDocument:', { + error: error.message, + code: error.code, + stack: error.stack, + }); + throw error; + } } } \ No newline at end of file diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index bdb51f4..f6d3754 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -15,6 +15,7 @@ import { ConfigModule } from '@nestjs/config'; import { AuthController } from './auth/auth.controller'; import { DocumentsController } from './documents/documents.controller'; import { JwtModule } from '@nestjs/jwt'; +import { EmailModule } from './email/email.module'; @Module({ imports: [ @@ -39,6 +40,7 @@ import { JwtModule } from '@nestjs/jwt'; ClientModule, S3Module, PrismaModule, + EmailModule, ], controllers: [AppController, AuthController, DocumentsController], providers: [ diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 8f0239e..e4b746a 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -6,6 +6,7 @@ import { UseGuards, Get, Request, + Logger, } from '@nestjs/common'; import { AuthService } from './auth.service'; import { LoginDto } from '../dto/login.dto'; @@ -15,29 +16,137 @@ import { AdminGuard } from './admin.guard'; //@UseGuards(JwtAuthGuard, AdminGuard) @Controller('auth') export class AuthController { - constructor(private authService: AuthService) {} + private readonly logger = new Logger(AuthController.name); + + constructor(private authService: AuthService) { + this.logger.log('AuthController initialized'); + } @Post('login') async login(@Body() loginDto: LoginDto) { - const user = await this.authService.validateUser( - loginDto.username, - loginDto.password, - ); - if (!user) { - throw new UnauthorizedException('Invalid credentials'); + this.logger.log('=== Login endpoint hit ==='); + this.logger.debug('Raw request body:', { + username: loginDto.username, + email: loginDto.email, + hasPassword: !!loginDto.password, + }); + + const email = loginDto.getEmail(); + this.logger.debug('Normalized login request:', { + email, + hasPassword: !!loginDto.password, + }); + + try { + this.logger.debug('Calling AuthService.validateUser...'); + const user = await this.authService.validateUser( + email, + loginDto.password, + ); + + if (!user) { + this.logger.warn(`Login failed: Invalid credentials for ${email}`); + throw new UnauthorizedException('Invalid email or password'); + } + + this.logger.debug('User validated successfully:', { + id: user.id, + email: user.email, + name: user.name, + isAdmin: user.isAdmin, + }); + + this.logger.debug('Calling AuthService.login...'); + const result = await this.authService.login(user); + + this.logger.debug('Login successful, returning response:', { + hasAccessToken: !!result.access_token, + user: { + id: result.user.id, + email: result.user.email, + name: result.user.name, + isAdmin: result.user.isAdmin, + }, + }); + + return result; + } catch (error) { + if (error instanceof UnauthorizedException) { + throw error; + } + + this.logger.error('Login failed:', { + error: error.message, + stack: error.stack, + body: { + username: loginDto.username, + email: loginDto.email, + hasPassword: !!loginDto.password, + }, + }); + throw new UnauthorizedException('Invalid email or password'); } - return this.authService.login(user); } @Post('register') async register(@Body() createUserDto: CreateUserDto) { - return this.authService.createUser(createUserDto); + console.log('=== Registration endpoint hit ==='); + this.logger.log('=== Registration endpoint hit ==='); + console.log('Registration request received:', createUserDto); + this.logger.log('Registration request received:', { + email: createUserDto.email, + name: createUserDto.name, + hasPassword: !!createUserDto.password + }); + + try { + console.log('Calling AuthService.createUser...'); + this.logger.log('Calling AuthService.createUser...'); + const result = await this.authService.createUser(createUserDto); + console.log('Registration successful:', result); + this.logger.log('Registration successful:', { + id: result.id, + email: result.email, + name: result.name, + }); + return result; + } catch (error) { + console.error('Registration failed:', error); + this.logger.error('Registration failed:', { + error: error.message, + code: error.code, + command: error.command, + stack: error.stack, + }); + throw error; + } } //@UseGuards(JwtAuthGuard) @Post('create-admin') async createAdmin(@Body() createUserDto: CreateUserDto) { - return this.authService.createUser(createUserDto, true); + this.logger.log('=== Create admin endpoint hit ==='); + this.logger.debug('Admin creation request received:', { + email: createUserDto.email, + name: createUserDto.name, + }); + + try { + this.logger.debug('Calling AuthService.createUser with isAdmin=true...'); + const result = await this.authService.createUser(createUserDto, true); + this.logger.debug('Admin creation successful:', { + id: result.id, + email: result.email, + name: result.name, + }); + return result; + } catch (error) { + this.logger.error('Admin creation failed:', { + error: error.message, + stack: error.stack, + }); + throw error; + } } @UseGuards(JwtAuthGuard) diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 6e2641a..433e225 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -1,23 +1,28 @@ import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; -import { LocalStrategy } from './local.strategy'; -import { JwtStrategy } from './jwt.strategy'; -//import { UsersModule } from '../users/users.module'; -import { PassportModule } from '@nestjs/passport'; +import { AuthController } from './auth.controller'; import { JwtModule } from '@nestjs/jwt'; -import { jwtConstants } from './constants'; -import { PrismaModule } from 'src/prisma/prisma.module'; +import { PrismaModule } from '../prisma/prisma.module'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtStrategy } from './jwt.strategy'; +import { EmailModule } from '../email/email.module'; @Module({ imports: [ - PassportModule, PrismaModule, - JwtModule.register({ - secret: jwtConstants.secret, - signOptions: { expiresIn: '60m' }, + ConfigModule, + EmailModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + signOptions: { expiresIn: '1h' }, + }), + inject: [ConfigService], }), ], - providers: [AuthService, LocalStrategy, JwtStrategy], + providers: [AuthService, JwtStrategy], + controllers: [AuthController], exports: [AuthService], }) export class AuthModule {} diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index ac68869..4988c4f 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -1,71 +1,201 @@ -import { Injectable, ConflictException } from '@nestjs/common'; +import { Injectable, ConflictException, Logger } from '@nestjs/common'; 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'; +import { EmailService } from '../email/email.service'; @Injectable() export class AuthService { + private readonly logger = new Logger(AuthService.name); + constructor( private prisma: PrismaService, private jwtService: JwtService, private configService: ConfigService, - ) {} + private emailService: EmailService, + ) { + this.logger.log('AuthService initialized with EmailService'); + } + + async validateUser(email: string, password: string): Promise { + this.logger.debug('Validating user:', { email }); + + try { + const user = await this.prisma.user.findUnique({ + where: { email }, + select: { + id: true, + email: true, + name: true, + password: true, + isAdmin: true, + }, + }); + + this.logger.debug('Database query result:', { + userFound: !!user, + userData: user ? { + id: user.id, + email: user.email, + name: user.name, + isAdmin: user.isAdmin, + } : null, + }); + + if (!user) { + this.logger.debug('User not found:', { email }); + return null; + } + + const isPasswordValid = await bcrypt.compare(password, user.password); + this.logger.debug('Password validation result:', { isPasswordValid }); + + if (!isPasswordValid) { + this.logger.debug('Invalid password for user:', { email }); + return null; + } - async validateUser(username: string, password: string): Promise { - const user = await this.prisma.user.findUnique({ - where: { email: username }, - }); - if (user && (await bcrypt.compare(password, user.password))) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { password, ...result } = user; + const { password: _, ...result } = user; + this.logger.debug('User validated successfully:', { + id: result.id, + email: result.email, + name: result.name, + isAdmin: result.isAdmin, + }); + return result; + } catch (error) { + this.logger.error('Error validating user:', { + error: error.message, + stack: error.stack, + }); + throw error; } - return null; } async login(user: any) { - // 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, { + this.logger.debug('Login called with user:', { + id: user.id, + email: user.email, + name: user.name, + isAdmin: user.isAdmin, + }); + + try { + const payload = { + email: user.email, + sub: user.id, + }; + + this.logger.debug('Generated JWT payload:', payload); + + const token = this.jwtService.sign(payload, { secret: this.configService.get('JWT_SECRET'), - }), - }; + }); + + this.logger.debug('JWT token generated successfully'); + + return { + access_token: token, + user: { + id: user.id, + email: user.email, + name: user.name, + isAdmin: user.isAdmin, + }, + }; + } catch (error) { + this.logger.error('Error generating JWT token:', { + error: error.message, + stack: error.stack, + }); + throw error; + } } async createUser( createUserDto: CreateUserDto, isAdmin: boolean = false, ): Promise { - const existingUser = await this.prisma.user.findUnique({ - where: { email: createUserDto.email }, - }); + console.log('=== Starting user creation process ==='); + this.logger.log('=== Starting user creation process ==='); + console.log('Creating user:', { ...createUserDto, isAdmin, password: '[REDACTED]' }); + this.logger.log('Creating user:', { ...createUserDto, isAdmin, password: '[REDACTED]' }); - if (existingUser) { - throw new ConflictException('Email already exists'); + try { + // Check for existing user + console.log('Checking for existing user...'); + this.logger.log('Checking for existing user...'); + const existingUser = await this.prisma.user.findUnique({ + where: { email: createUserDto.email }, + }); + + if (existingUser) { + console.log('User already exists:', createUserDto.email); + this.logger.warn('User already exists:', createUserDto.email); + throw new ConflictException('Email already exists'); + } + + // Hash password + console.log('Hashing password...'); + this.logger.log('Hashing password...'); + const hashedPassword = await bcrypt.hash(createUserDto.password, 10); + + // Create user + console.log('Creating user in database...'); + this.logger.log('Creating user in database...'); + const newUser = await this.prisma.user.create({ + data: { + email: createUserDto.email, + password: hashedPassword, + name: createUserDto.name, + isAdmin: isAdmin, + }, + }); + + console.log('User created successfully:', { id: newUser.id, email: newUser.email }); + this.logger.log('User created successfully:', { id: newUser.id, email: newUser.email }); + + // Send welcome email + console.log('Attempting to send welcome email...'); + this.logger.log('Attempting to send welcome email...'); + + try { + console.log('Calling EmailService.sendWelcomeEmail...'); + this.logger.log('Calling EmailService.sendWelcomeEmail...'); + await this.emailService.sendWelcomeEmail(newUser.email, newUser.name); + console.log('Welcome email sent successfully'); + this.logger.log('Welcome email sent successfully'); + } catch (emailError) { + console.error('Failed to send welcome email:', emailError); + this.logger.error('Failed to send welcome email:', { + error: emailError.message, + code: emailError.code, + command: emailError.command, + response: emailError.response, + stack: emailError.stack, + }); + // Don't throw the error, just log it + } + + // Return user data + const { password, ...result } = newUser; + console.log('=== User creation completed ==='); + this.logger.log('=== User creation completed ==='); + return result; + } catch (error) { + console.error('Error in createUser:', error); + this.logger.error('Error in createUser:', { + error: error.message, + code: error.code, + command: error.command, + stack: error.stack, + }); + throw error; } - - const hashedPassword = await bcrypt.hash(createUserDto.password, 10); - - const newUser = await this.prisma.user.create({ - data: { - email: createUserDto.email, - password: hashedPassword, - name: createUserDto.name, - isAdmin: isAdmin, - }, - }); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { password, ...result } = newUser; - console.log(result); - return result; } // async getUserInfo(userId: number) { diff --git a/backend/src/auth/jwt.strategy.ts b/backend/src/auth/jwt.strategy.ts index b166ae8..b6114cb 100644 --- a/backend/src/auth/jwt.strategy.ts +++ b/backend/src/auth/jwt.strategy.ts @@ -1,19 +1,63 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(configService: ConfigService) { + private readonly logger = new Logger(JwtStrategy.name); + + constructor( + configService: ConfigService, + private prisma: PrismaService, + ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.get('JWT_SECRET'), }); + this.logger.log('JwtStrategy initialized'); } async validate(payload: any) { - return { userId: payload.sub, username: payload.username }; + this.logger.debug('Validating JWT payload:', payload); + + try { + const user = await this.prisma.user.findUnique({ + where: { id: payload.sub }, + select: { + id: true, + email: true, + name: true, + isAdmin: true, + }, + }); + + if (!user) { + this.logger.warn('JWT validation failed: User not found:', { userId: payload.sub }); + throw new UnauthorizedException('User not found'); + } + + this.logger.debug('JWT validation successful:', { + userId: user.id, + email: user.email, + name: user.name, + isAdmin: user.isAdmin, + }); + + return { + userId: user.id, + email: user.email, + name: user.name, + isAdmin: user.isAdmin, + }; + } catch (error) { + this.logger.error('JWT validation error:', { + error: error.message, + stack: error.stack, + }); + throw new UnauthorizedException('Invalid token'); + } } } diff --git a/backend/src/documents/documents.controller.ts b/backend/src/documents/documents.controller.ts index 3895f25..9d5c8ce 100644 --- a/backend/src/documents/documents.controller.ts +++ b/backend/src/documents/documents.controller.ts @@ -1,8 +1,9 @@ -import { Controller, Get, Param, Req, Res, UseGuards, Logger, Request } from '@nestjs/common'; +import { Controller, Get, Param, Req, Res, UseGuards, Logger, Request, Post, UseInterceptors, UploadedFile, Body } from '@nestjs/common'; import { Response } from 'express'; import { DocumentsService } from './documents.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { S3Service } from '../s3/s3.service'; +import { FileInterceptor } from '@nestjs/platform-express'; interface S3File { buffer: Buffer; @@ -18,7 +19,51 @@ export class DocumentsController { constructor( private readonly documentsService: DocumentsService, private readonly s3Service: S3Service - ) {} + ) { + this.logger.log('DocumentsController initialized'); + } + + @Post('upload') + @UseGuards(JwtAuthGuard) + @UseInterceptors(FileInterceptor('file')) + async uploadDocument( + @UploadedFile() file: Express.Multer.File, + @Body('title') title: string, + @Body('sharedWithId') sharedWithId: string, + @Request() req, + ) { + this.logger.log('=== Document upload endpoint hit ==='); + this.logger.debug('Upload request received:', { + fileName: file?.originalname, + fileSize: file?.size, + title, + sharedWithId, + uploadedById: req.user.id, + }); + + try { + const result = await this.documentsService.uploadDocument( + file, + title, + Number(sharedWithId), + req.user.id, + ); + + this.logger.debug('Document upload successful:', { + documentId: result.id, + title: result.title, + s3Key: result.s3Key, + }); + + return result; + } catch (error) { + this.logger.error('Document upload failed:', { + error: error.message, + stack: error.stack, + }); + throw error; + } + } @Get('shared/:userId') @UseGuards(JwtAuthGuard) diff --git a/backend/src/documents/documents.module.ts b/backend/src/documents/documents.module.ts new file mode 100644 index 0000000..9172899 --- /dev/null +++ b/backend/src/documents/documents.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { DocumentsService } from './documents.service'; +import { DocumentsController } from './documents.controller'; +import { PrismaModule } from '../prisma/prisma.module'; +import { S3Module } from '../s3/s3.module'; +import { EmailModule } from '../email/email.module'; + +@Module({ + imports: [PrismaModule, S3Module, EmailModule], + controllers: [DocumentsController], + providers: [DocumentsService], + exports: [DocumentsService], +}) +export class DocumentsModule {} \ No newline at end of file diff --git a/backend/src/documents/documents.service.ts b/backend/src/documents/documents.service.ts index 48b9a3d..6558714 100644 --- a/backend/src/documents/documents.service.ts +++ b/backend/src/documents/documents.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger, NotFoundException, ForbiddenException } from '@nest import { PrismaService } from '../prisma/prisma.service'; import { S3Service } from '../s3/s3.service'; import { Document, User, Prisma } from '@prisma/client'; +import { EmailService } from '../email/email.service'; @Injectable() export class DocumentsService { @@ -9,8 +10,16 @@ export class DocumentsService { constructor( private readonly prisma: PrismaService, - private readonly s3Service: S3Service - ) {} + private readonly s3Service: S3Service, + private readonly emailService: EmailService, + ) { + this.logger.log('DocumentsService initialized with EmailService'); + this.logger.debug('EmailService instance:', { + exists: !!this.emailService, + type: typeof this.emailService, + methods: Object.keys(Object.getPrototypeOf(this.emailService)), + }); + } async findDocumentByS3Key(s3Key: string) { return this.prisma.document.findFirst({ @@ -85,36 +94,139 @@ export class DocumentsService { 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, - }, - }, - }, + this.logger.log('=== Starting document upload process ==='); + this.logger.debug('Upload parameters:', { + title, + sharedWithId, + uploadedById, + fileName: file.originalname, + fileSize: file.size, }); + + try { + this.logger.debug('Uploading file to S3...'); + const s3Key = await this.s3Service.uploadFile(file, 'documents'); + this.logger.debug(`File uploaded to S3 successfully with key: ${s3Key}`); + + this.logger.debug('Creating document record in database...'); + const document = await 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, + }, + }, + }, + }); + this.logger.debug('Document record created:', { + id: document.id, + title: document.title, + uploadedBy: document.uploadedBy, + sharedWith: document.sharedWith, + }); + + // Send email notifications + this.logger.log('=== Starting email notification process ==='); + + // Notify the user who the document is shared with + const sharedUser = document.sharedWith[0]; + if (sharedUser) { + this.logger.debug(`Preparing to send notification to shared user:`, { + id: sharedUser.id, + email: sharedUser.email, + name: sharedUser.name, + }); + try { + this.logger.debug('Calling EmailService.sendDocumentNotification for shared user...'); + await this.emailService.sendDocumentNotification( + sharedUser.email, + sharedUser.name, + document.title, + 'shared' + ); + this.logger.debug('Shared user notification sent successfully'); + } catch (error) { + this.logger.error('Failed to send notification to shared user:', { + error: error.message, + code: error.code, + command: error.command, + stack: error.stack, + }); + // Log the email service instance to verify it's properly injected + this.logger.debug('EmailService instance:', { + exists: !!this.emailService, + type: typeof this.emailService, + methods: Object.keys(Object.getPrototypeOf(this.emailService)), + }); + } + } else { + this.logger.warn('No shared user found in the document record'); + } + + // Notify the uploader + const uploader = document.uploadedBy; + if (uploader) { + this.logger.debug(`Preparing to send notification to uploader:`, { + id: uploader.id, + email: uploader.email, + name: uploader.name, + }); + try { + this.logger.debug('Calling EmailService.sendDocumentNotification for uploader...'); + await this.emailService.sendDocumentNotification( + uploader.email, + uploader.name, + document.title, + 'uploaded' + ); + this.logger.debug('Uploader notification sent successfully'); + } catch (error) { + this.logger.error('Failed to send notification to uploader:', { + error: error.message, + code: error.code, + command: error.command, + stack: error.stack, + }); + // Log the email service instance to verify it's properly injected + this.logger.debug('EmailService instance:', { + exists: !!this.emailService, + type: typeof this.emailService, + methods: Object.keys(Object.getPrototypeOf(this.emailService)), + }); + } + } else { + this.logger.warn('No uploader found in the document record'); + } + + this.logger.log('=== Document upload process completed ==='); + return document; + } catch (error) { + this.logger.error('Error in uploadDocument:', { + error: error.message, + code: error.code, + stack: error.stack, + }); + throw error; + } } } diff --git a/backend/src/dto/create-user.dto.ts b/backend/src/dto/create-user.dto.ts index 8cf8a74..7edbf87 100644 --- a/backend/src/dto/create-user.dto.ts +++ b/backend/src/dto/create-user.dto.ts @@ -1,4 +1,5 @@ import { IsString, IsEmail, MinLength, IsBoolean } from 'class-validator'; +import { Transform } from 'class-transformer'; export class CreateUserDto { @IsString() @@ -7,6 +8,7 @@ export class CreateUserDto { @IsEmail() email: string; + @Transform(({ value }) => value?.trim()) @IsString() @MinLength(6) password: string; diff --git a/backend/src/dto/login.dto.ts b/backend/src/dto/login.dto.ts index 28f7812..3222663 100644 --- a/backend/src/dto/login.dto.ts +++ b/backend/src/dto/login.dto.ts @@ -1,11 +1,25 @@ -import { IsString, IsNotEmpty } from 'class-validator'; +import { IsString, IsNotEmpty, IsEmail, ValidateIf } from 'class-validator'; +import { Transform } from 'class-transformer'; export class LoginDto { + @ValidateIf(o => !o.email) @IsString() @IsNotEmpty() - username: string; + username?: string; + @ValidateIf(o => !o.username) + @IsString() + @IsNotEmpty() + @IsEmail() + email?: string; + + @Transform(({ value }) => value?.trim()) @IsString() @IsNotEmpty() password: string; + + // Helper method to get the email (either from email or username field) + getEmail(): string { + return this.email || this.username; + } } diff --git a/backend/src/email/email.module.ts b/backend/src/email/email.module.ts new file mode 100644 index 0000000..8b30b28 --- /dev/null +++ b/backend/src/email/email.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { EmailService } from './email.service'; + +@Module({ + imports: [ConfigModule], + providers: [EmailService], + exports: [EmailService], +}) +export class EmailModule {} \ No newline at end of file diff --git a/backend/src/email/email.service.ts b/backend/src/email/email.service.ts new file mode 100644 index 0000000..7f78fc4 --- /dev/null +++ b/backend/src/email/email.service.ts @@ -0,0 +1,187 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as nodemailer from 'nodemailer'; + +@Injectable() +export class EmailService { + private transporter: nodemailer.Transporter; + private readonly logger = new Logger(EmailService.name); + private readonly from: string; + + constructor(private configService: ConfigService) { + console.log('Initializing EmailService...'); // Direct console log for debugging + this.logger.log('Initializing EmailService...'); + + // Load config + const host = this.configService.get('SMTP_HOST'); + const port = this.configService.get('SMTP_PORT'); + const user = this.configService.get('SMTP_USER'); + const pass = this.configService.get('SMTP_PASS'); + this.from = this.configService.get('EMAIL_FROM'); + + console.log('Email Config:', { host, port, user, from: this.from }); // Direct console log + this.logger.log('Email Config:', { host, port, user, from: this.from }); + + // Create transporter with Gmail settings + this.transporter = nodemailer.createTransport({ + host: 'smtp.gmail.com', + port: 587, // Use STARTTLS port + secure: false, // Use STARTTLS + auth: { user, pass }, + debug: true, // Enable debug logs + logger: true // Enable transport level logging + }); + + // Verify connection + this.verifyConnection(); + } + + private async verifyConnection() { + try { + console.log('Verifying SMTP connection...'); // Direct console log + this.logger.log('Verifying SMTP connection...'); + + const verification = await this.transporter.verify(); + + console.log('SMTP connection verified successfully!', verification); // Direct console log + this.logger.log('SMTP connection verified successfully!', verification); + } catch (error) { + console.error('SMTP connection failed:', error); // Direct console log + this.logger.error('SMTP connection failed:', { + error: error.message, + code: error.code, + command: error.command, + response: error.response, + responseCode: error.responseCode, + stack: error.stack, + }); + throw error; + } + } + + async sendWelcomeEmail(userEmail: string, username: string): Promise { + console.log(`Sending welcome email to ${userEmail}...`); // Direct console log + this.logger.log(`Sending welcome email to ${userEmail}...`); + + const mailOptions = { + from: `"IMK Platform" <${this.from}>`, + to: userEmail, + subject: 'Welcome to IMK Platform!', + html: ` +
+

Welcome to IMK Platform!

+

Dear ${username},

+

Thank you for joining IMK Platform. We're excited to have you on board!

+

You can now start using our platform to manage and share your documents securely.

+

If you have any questions or need assistance, please don't hesitate to contact our support team.

+

Best regards,
The IMK Team

+
+ ` + }; + + try { + console.log('Attempting to send email with options:', mailOptions); // Direct console log + this.logger.log('Attempting to send email with options:', { + to: mailOptions.to, + from: mailOptions.from, + subject: mailOptions.subject + }); + + const info = await this.transporter.sendMail(mailOptions); + + console.log('Email sent successfully:', info); // Direct console log + this.logger.log('Email sent successfully:', { + messageId: info.messageId, + response: info.response, + accepted: info.accepted, + rejected: info.rejected, + envelope: info.envelope, + }); + } catch (error) { + console.error('Failed to send email:', error); // Direct console log + this.logger.error('Failed to send email:', { + error: error.message, + code: error.code, + command: error.command, + response: error.response, + responseCode: error.responseCode, + stack: error.stack, + }); + throw error; + } + } + + async sendDocumentNotification( + userEmail: string, + username: string, + documentName: string, + action: 'uploaded' | 'shared', + ): Promise { + console.log('=== Starting document notification email process ==='); + this.logger.log('=== Starting document notification email process ==='); + console.log(`Preparing notification for ${userEmail}, document: ${documentName}, action: ${action}`); + this.logger.log(`Preparing notification for ${userEmail}, document: ${documentName}, action: ${action}`); + + const actionText = action === 'uploaded' ? 'uploaded' : 'shared with you'; + const mailOptions = { + from: `"IMK Platform" <${this.from}>`, + to: userEmail, + subject: `New Document ${actionText} - IMK Platform`, + html: ` +
+

New Document Notification

+

Dear ${username},

+

A new document "${documentName}" has been ${actionText} on the IMK Platform.

+

You can access this document by logging into your account.

+

If you have any questions or concerns, please contact our support team.

+

Best regards,
The IMK Team

+
+ `, + }; + + try { + console.log('Sending document notification email with options:', { + to: mailOptions.to, + from: mailOptions.from, + subject: mailOptions.subject, + documentName, + action + }); + this.logger.log('Sending document notification email with options:', { + to: mailOptions.to, + from: mailOptions.from, + subject: mailOptions.subject, + documentName, + action + }); + + const info = await this.transporter.sendMail(mailOptions); + + console.log('Document notification email sent successfully:', { + messageId: info.messageId, + response: info.response, + accepted: info.accepted, + rejected: info.rejected, + envelope: info.envelope, + }); + this.logger.log('Document notification email sent successfully:', { + messageId: info.messageId, + response: info.response, + accepted: info.accepted, + rejected: info.rejected, + envelope: info.envelope, + }); + } catch (error) { + console.error('Failed to send document notification email:', error); + this.logger.error('Failed to send document notification email:', { + error: error.message, + code: error.code, + command: error.command, + response: error.response, + responseCode: error.responseCode, + stack: error.stack, + }); + throw error; + } + } +} \ No newline at end of file diff --git a/backend/src/main.ts b/backend/src/main.ts index da5451c..75e134a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,9 +1,46 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { Logger, ValidationPipe } from '@nestjs/common'; async function bootstrap() { - const app = await NestFactory.create(AppModule); - app.enableCors(); - await app.listen(3000); + const logger = new Logger('Bootstrap'); + logger.log('Starting application...'); + + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log', 'debug', 'verbose'], // Enable all log levels + }); + + logger.log('Configuring application...'); + + // Enable validation with detailed error messages + app.useGlobalPipes(new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + enableDebugMessages: true, // Add detailed validation error messages + validationError: { + target: false, + value: false, + }, + })); + + // Enable CORS with credentials + app.enableCors({ + origin: true, // or specify your frontend URL + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'], + }); + + const port = process.env.PORT || 3000; + logger.log(`Starting server on port ${port}...`); + + await app.listen(port); + logger.log(`Application is running on: ${await app.getUrl()}`); } + +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); +}); + bootstrap(); diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 28716b7..58e60f5 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -61,7 +61,7 @@ export const downloadDocument = async (key) => { export const createUser = (userData) => { - return api.post('/admin/users', { + return api.post('/auth/register', { name: userData.name, email: userData.email, password: userData.password, @@ -69,7 +69,7 @@ export const createUser = (userData) => { }); }; export const login = (username, password) => api.post('/auth/login', { username, password }); -export const shareDocument = (documentId, userIds) => api.post(`/admin/documents/${documentId}/share`, { userIds }); +export const shareDocument = (documentId, userId) => api.post(`/admin/documents/${documentId}/share`, { userId }); export const updateDocumentStatus = (documentId, status) => api.put(`/admin/documents/${documentId}/status`, { status }); export const uploadDocument = async (formData) => {