Merge branch 'mailfunc'

This commit is contained in:
dimitar 2025-02-25 20:47:46 +01:00
commit a0eb8e1ed8
20 changed files with 1110 additions and 163 deletions

View File

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

View File

@ -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",

View File

@ -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",

View File

@ -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) {

View File

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

View File

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

View File

@ -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: [

View File

@ -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)

View File

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

View File

@ -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<any> {
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<any> {
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<string>('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<any> {
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) {

View File

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

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<string>('SMTP_HOST');
const port = this.configService.get<number>('SMTP_PORT');
const user = this.configService.get<string>('SMTP_USER');
const pass = this.configService.get<string>('SMTP_PASS');
this.from = this.configService.get<string>('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<void> {
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: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Welcome to IMK Platform!</h2>
<p>Dear ${username},</p>
<p>Thank you for joining IMK Platform. We're excited to have you on board!</p>
<p>You can now start using our platform to manage and share your documents securely.</p>
<p>If you have any questions or need assistance, please don't hesitate to contact our support team.</p>
<p>Best regards,<br>The IMK Team</p>
</div>
`
};
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<void> {
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: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2>New Document Notification</h2>
<p>Dear ${username},</p>
<p>A new document "${documentName}" has been ${actionText} on the IMK Platform.</p>
<p>You can access this document by logging into your account.</p>
<p>If you have any questions or concerns, please contact our support team.</p>
<p>Best regards,<br>The IMK Team</p>
</div>
`,
};
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;
}
}
}

View File

@ -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();

View File

@ -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) => {