mailFunc #2

Open
dimitar wants to merge 5 commits from mailFunc into main
22 changed files with 8893 additions and 375 deletions

View File

@ -6,8 +6,16 @@
DATABASE_URL="postgresql://root:admin@localhost:5432/imk?schema=public"
JWT_SECRET=some-secret
JWT_RESET_SECRET=some-reset-secret-key
AWS_REGION=EU2
AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6
AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4
AWS_S3_BUCKET_NAME=imk-data
AWS_ENDPOINT_URL=https://eu2.contabostorage.com
SMTP_HOST=imk.mk
SMTP_PORT=465
SMTP_USER=mailer@imk.mk
SMTP_PASSWORD=76Avtostoperski76
SMTP_FROM=mailer@imk.mk
FRONTEND_URL=http://localhost:5173
ADMIN_EMAIL=petrovskidimitar@yandex.com

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@
"@aws-sdk/client-s3": "^3.679.0",
"@aws-sdk/lib-storage": "^3.679.0",
"@aws-sdk/s3-request-presigner": "^3.679.0",
"@nestjs-modules/mailer": "^1.6.1",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
@ -36,6 +37,7 @@
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"nodemailer": "^6.9.16",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
@ -45,13 +47,14 @@
"typeorm": "^0.3.20"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/cli": "^10.4.5",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/supertest": "^2.0.12",

View File

@ -1,118 +1,3 @@
// 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,
@ -160,13 +45,13 @@ export class AdminController {
}
@Post('documents')
@UseInterceptors(FileInterceptor('file'))
async uploadDocument(
@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');
}
@ -180,6 +65,7 @@ async uploadDocument(
throw new BadRequestException('sharedWithId and uploadedById must be valid numbers');
}
const document = await this.adminService.uploadDocument(
file,
title,
@ -194,25 +80,6 @@ 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

@ -4,12 +4,14 @@ 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 'src/email/email.service';
@Injectable()
export class AdminService {
constructor(
private readonly prisma: PrismaService,
private readonly s3Service: S3Service,
private readonly emailService: EmailService,
) {}
async getAllDocuments() {
@ -41,14 +43,22 @@ export class AdminService {
}
async createUser(createUserDto: CreateUserDto) {
try {
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
return this.prisma.user.create({
data: {
...createUserDto,
password: hashedPassword,
},
const user = await this.prisma.user.create({
data: { ...createUserDto, password: hashedPassword},
});
try {
await this.emailService.sendWelcomeEmail(user.email, user.name );
console.log('Welcome email sent successfully');
} catch (error) {
console.log('Error sending welcome email:', error);
}
return user;
} catch (error) {
console.error('Error creating user:', error );
throw error;
}
}
async updateDocument(
@ -71,14 +81,32 @@ export class AdminService {
}
async shareDocument(documentId: number, userId: number) {
return this.prisma.document.update({
const document = await this.prisma.document.update({
where: { id: documentId },
data: {
sharedWith: {
connect: { id: userId },
},
},
include: {
uploadedBy: true,
sharedWith: true,
},
});
const sharedWithUser = await this.prisma.user.findUnique({
where: { id: userId },
});
// Send email notification
await this.emailService.sendDocumentSharedNotification(
sharedWithUser.email,
document.title,
document.uploadedBy.name,
);
console.log('Document shared with user:', sharedWithUser.email);
return document;
}
async updateDocumentStatus(documentId: number, status: string) {
@ -93,7 +121,9 @@ export class AdminService {
title: string,
sharedWithId: number,
uploadedById: number
) {
)
{
const s3Key = await this.s3Service.uploadFile(file, 'documents');
return this.prisma.document.create({
@ -126,4 +156,5 @@ export class AdminService {
},
});
}
}

View File

@ -15,18 +15,11 @@ 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: [
// TypeOrmModule.forRoot({
// type: 'postgres',
// host: 'localhost',
// port: 5432,
// username: 'root',
// password: 'admin',
// database: 'imk',
// synchronize: true,
// }),
EmailModule,
ConfigModule.forRoot({
isGlobal: true,
}),

View File

@ -6,6 +6,7 @@ import {
UseGuards,
Get,
Request,
BadRequestException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from '../dto/login.dto';
@ -37,7 +38,24 @@ export class AuthController {
//@UseGuards(JwtAuthGuard)
@Post('create-admin')
async createAdmin(@Body() createUserDto: CreateUserDto) {
return this.authService.createUser(createUserDto, true);
return this.authService.createUser(createUserDto);
}
@Post('forgot-password')
async forgotPassword(@Body('email') email: string) {
if (!email) {
throw new BadRequestException('Email is required');
}
return this.authService.requestPasswordReset(email);
}
@Post('reset-password')
async resetPassword(
@Body() body: { token: string; newPassword: string },
) {
if (!body.token || !body.newPassword) {
throw new BadRequestException('Token and new password are required');
}
return this.authService.resetPassword(body.token, body.newPassword);
}
@UseGuards(JwtAuthGuard)

View File

@ -7,6 +7,7 @@ import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { PrismaModule } from 'src/prisma/prisma.module';
import { EmailModule } from 'src/email/email.module';
@Module({
imports: [
@ -16,6 +17,7 @@ import { PrismaModule } from 'src/prisma/prisma.module';
secret: jwtConstants.secret,
signOptions: { expiresIn: '60m' },
}),
EmailModule,
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],

View File

@ -1,9 +1,10 @@
import { Injectable, ConflictException } from '@nestjs/common';
import { Injectable, ConflictException, NotFoundException, BadRequestException } 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 'src/email/email.service';
@Injectable()
export class AuthService {
@ -11,6 +12,7 @@ export class AuthService {
private prisma: PrismaService,
private jwtService: JwtService,
private configService: ConfigService,
private emailService: EmailService,
) {}
async validateUser(username: string, password: string): Promise<any> {
@ -39,46 +41,51 @@ export class AuthService {
};
}
async createUser(
createUserDto: CreateUserDto,
isAdmin: boolean = false,
): Promise<any> {
const existingUser = await this.prisma.user.findUnique({
where: { email: createUserDto.email },
});
if (existingUser) {
throw new ConflictException('Email already exists');
}
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
const newUser = await this.prisma.user.create({
async createUser(createUserDto: CreateUserDto) {
const user = await this.prisma.user.create({
data: {
email: createUserDto.email,
password: hashedPassword,
name: createUserDto.name,
isAdmin: isAdmin,
...createUserDto,
password: await bcrypt.hash(createUserDto.password, 10),
},
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, ...result } = newUser;
console.log(result);
return result;
// Send welcome email
await this.emailService.sendWelcomeEmail(user.email, user.name);
return user;
}
// async getUserInfo(userId: number) {
// return this.prisma.user.findUnique({
// where: { id: userId },
// select: {
// id: true,
// name: true,
// email: true,
// isAdmin: true,
// },
// });
// }
async requestPasswordReset(email: string) {
const user = await this.prisma.user.findUnique({ where: { email } });
if (!user) {
throw new NotFoundException('User not found');
}
const resetToken = this.jwtService.sign(
{ email, userId: user.id },
{ expiresIn: '1h', secret: process.env.JWT_RESET_SECRET },
);
await this.emailService.sendPasswordResetEmail(email, resetToken);
return { message: 'Password reset instructions sent to your email' };
}
async resetPassword(token: string, newPassword: string) {
try {
const payload = this.jwtService.verify(token, {
secret: process.env.JWT_RESET_SECRET,
});
const hashedPassword = await bcrypt.hash(newPassword, 10);
await this.prisma.user.update({
where: { email: payload.email },
data: { password: hashedPassword },
});
return { message: 'Password reset successful' };
} catch (error) {
throw new BadRequestException('Invalid or expired reset token');
}
}
async getUserInfo(userId: number) {
if (!userId) {
throw new Error('User ID is required');

View File

@ -2,14 +2,17 @@ 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 {
private readonly logger = new Logger(DocumentsService.name);
//private readonly Logger = new Logger(DocumentsService.user);
constructor(
private readonly prisma: PrismaService,
private readonly s3Service: S3Service
private readonly s3Service: S3Service,
private readonly emailService: EmailService
) {}
async findDocumentByS3Key(s3Key: string) {
@ -116,5 +119,7 @@ export class DocumentsService {
},
},
});
console.log(title, sharedWithId, uploadedById)
}
}

View File

@ -0,0 +1,36 @@
import { Controller, Post, Body } from '@nestjs/common';
import { EmailService } from './email.service';
// import { MailerService } from '@nestjs-modules/mailer';
class ContactFormDto {
name: string;
email: string;
message: string;
}
@Controller('email')
export class EmailController {
constructor(private emailService: EmailService) {}
@Post()
async sendContactEmail(@Body() contactData: {
name: string;
email: string;
message: string;
}) {
await this.emailService.sendContactEmail(
contactData.name,
contactData.email,
contactData.message
);
return { message: 'Contact form submitted successfully' };
}
@Post('contact')
async handleContactForm(@Body() contactData: ContactFormDto) {
await this.emailService.sendContactEmail(
contactData.name,
contactData.email,
contactData.message
);
return { message: 'Thank you for your message. We will contact you soon.' };
}
}

View File

@ -0,0 +1,32 @@
import { Module } from '@nestjs/common';
import { MailerModule } from '@nestjs-modules/mailer';
import { EmailService } from './email.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { EmailController } from './email.controller';
@Module({
imports: [
MailerModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (config: ConfigService) => ({
transport: {
host: config.get('SMTP_HOST'),
port: config.get('SMTP_PORT'),
secure: true,
auth: {
user: config.get('SMTP_USER'),
pass: config.get('SMTP_PASSWORD'),
},
},
defaults: {
from: `"No Reply" <${config.get('SMTP_FROM')}>`,
},
}),
inject: [ConfigService],
}),
],
controllers: [EmailController],
providers: [EmailService],
exports: [EmailService],
})
export class EmailModule {}

View File

@ -0,0 +1,82 @@
import { Injectable, Post, Body } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
@Injectable()
export class EmailService {
constructor(private mailerService: MailerService) {}
async sendWelcomeEmail(email: string, name: string) {
await this.mailerService.sendMail({
to: email,
subject: 'Welcome to IMK Platform',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #2563eb;">Welcome to IMK Platform!</h1>
<p>Dear ${name},</p>
<p>Thank you for joining IMK Platform. Your account has been created successfully.</p>
<p>You can now log in to access your documents and start collaborating with your team.</p>
<p>If you have any questions or need assistance, please don't hesitate to contact our support team.</p>
<div style="margin: 30px 0;">
<a href="${process.env.FRONTEND_URL}/login"
style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px;">
Login to Your Account
</a>
</div>
<p>Best regards,<br>IMK Team</p>
</div>
`,
});
}
async sendPasswordResetEmail(email: string, resetToken: string) {
const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
await this.mailerService.sendMail({
to: email,
subject: 'Password Reset Request',
html: `
<h1>Password Reset</h1>
<p>Click the link below to reset your password:</p>
<a href="${resetLink}">Reset Password</a>
<p>This link will expire in 1 hour.</p>
`,
});
}
async sendDocumentSharedNotification(email: string, documentTitle: string, sharedByName: string) {
await this.mailerService.sendMail({
to: email,
subject: 'New Document Shared With You',
html: `
<h1>New Document Available</h1>
<p>${sharedByName} has shared a document with you: "${documentTitle}"</p>
<p>Login to your account to view the document.</p>
`,
});
}
async sendContactEmail(name: string, email: string, message: string) {
// Send to admin
await this.mailerService.sendMail({
to: process.env.ADMIN_EMAIL,
subject: `New Contact Form Submission from ${name}`,
html: `
<h2>New Contact Form Submission</h2>
<p><strong>From:</strong> ${name}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Message:</strong></p>
<p>${message}</p>
`,
});
await this.mailerService.sendMail({
to: email,
subject: 'Thank you for contacting us',
html: `
<h2>Thank you for contacting us</h2>
<p>Dear ${name},</p>
<p>We have received your message and will get back to you soon.</p>
<p>Your message:</p>
<blockquote>${message}</blockquote>
<p>Best regards,<br>IMK Team</p>
`,
});
}
}

View File

@ -16,6 +16,8 @@ import AdminPanel from './components/adminPanel/AdminPanel';
import Dashboard from './components/dashboard/Dashboard';
import Login from './components/login/login';
import ProtectedRoute from './components/protectedRoute/ProtectedRoute';
import ForgotPassword from './components/ForgotPassword';
import ResetPassword from './components/ResetPassword';
function App() {
@ -33,6 +35,8 @@ function App() {
<Route path='/ultrasound' element={<UltraSound />} />
<Route path='/gallery' element={<Gallery />} />
<Route path='/certificates' element={<Certificates />} />
<Route path='/forgot-password' element={<ForgotPassword />} />
<Route path='/reset-password' element={<ResetPassword />} />
<Route path='/clients' element={
<ProtectedRoute>
<Clients />

View File

@ -0,0 +1,69 @@
import { useState } from 'react';
import axios from 'axios';
export default function ForgotPassword() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState({ type: '', message: '' });
const handleSubmit = async (e) => {
e.preventDefault();
setStatus({ type: 'loading', message: 'Sending reset instructions...' });
try {
await axios.post(`http://localhost:3000/auth/forgot-password`, { email });
setStatus({
type: 'success',
message: 'Password reset instructions have been sent to your email.',
});
} catch (error) {
setStatus({
type: 'error',
message: error.response?.data?.message || 'An error occurred. Please try again.',
});
}
};
return (
<div className="min-h-screen bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="text-center text-3xl font-extrabold text-white">
Reset your password
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-white">
Email address
</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 bg-gray-700 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
{status.message && (
<div className={`text-sm ${status.type === 'error' ? 'text-red-400' : 'text-green-400'}`}>
{status.message}
</div>
)}
<button
type="submit"
disabled={status.type === 'loading'}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{status.type === 'loading' ? 'Sending...' : 'Send Reset Instructions'}
</button>
</form>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,94 @@
import { useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import axios from 'axios';
export default function ResetPassword() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [status, setStatus] = useState({ type: '', message: '' });
const handleSubmit = async (e) => {
e.preventDefault();
if (password !== confirmPassword) {
setStatus({ type: 'error', message: 'Passwords do not match' });
return;
}
setStatus({ type: 'loading', message: 'Resetting password...' });
const token = searchParams.get('token');
try {
await axios.post(`http://localhost:3000/auth/reset-password`, {
token,
newPassword: password,
});
setStatus({ type: 'success', message: 'Password reset successful!' });
setTimeout(() => navigate('/login'), 2000);
} catch (error) {
setStatus({
type: 'error',
message: error.response?.data?.message || 'An error occurred. Please try again.',
});
}
};
return (
<div className="min-h-screen bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="text-center text-3xl font-extrabold text-white">
Reset your password
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="password" className="block text-sm font-medium text-white">
New Password
</label>
<input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 bg-gray-700 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-white">
Confirm Password
</label>
<input
id="confirmPassword"
type="password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 bg-gray-700 text-white shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
/>
</div>
{status.message && (
<div className={`text-sm ${status.type === 'error' ? 'text-red-400' : 'text-green-400'}`}>
{status.message}
</div>
)}
<button
type="submit"
disabled={status.type === 'loading'}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{status.type === 'loading' ? 'Resetting...' : 'Reset Password'}
</button>
</form>
</div>
</div>
</div>
);
}

View File

@ -1,8 +1,9 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';
import { getSharedDocuments } from '../../services/api';
import { format } from 'date-fns';
import { FiFolder, FiFile, FiDownload, FiChevronRight, FiLoader } from 'react-icons/fi';
import { FiFolder, FiFile, FiDownload, FiChevronRight, FiLoader, FiUser, FiKey, FiMail } from 'react-icons/fi';
import { downloadDocument } from '../../services/api';
function Dashboard() {
@ -11,6 +12,13 @@ function Dashboard() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const { user } = useAuth();
const [userInfo, setUserInfo] = useState(null);
useEffect(() => {
if (user) {
setUserInfo(user);
}
}, [user]);
useEffect(() => {
const fetchDocuments = async () => {
@ -88,10 +96,47 @@ function Dashboard() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-6">
<div className="max-w-7xl mx-auto">
<header className="mb-8 mt-20">
{/* <header className="mb-8 mt-20">
<h1 className="text-3xl font-bold text-white mb-2">Your Documents</h1>
<p className="text-gray-400">Access and manage your shared documents</p>
</header>
</header> */}
{/* User Info Card */}
<div className="bg-gray-800 rounded-lg shadow-lg p-6 mb-8">
<div className="flex items-center space-x-4">
<div className="p-3 bg-indigo-600 rounded-full">
<FiUser className="w-6 h-6 text-white" />
</div>
<div>
<h2 className="text-xl font-bold text-white">Welcome, {userInfo?.name}</h2>
<p className="text-gray-400">{userInfo?.email}</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Link
to="/reset-password"
className="flex items-center space-x-3 p-4 bg-gray-800 rounded-lg hover:bg-gray-700 transition-colors"
>
<FiKey className="w-5 h-5 text-indigo-500" />
<div>
<h3 className="text-white font-semibold">Reset Password</h3>
<p className="text-sm text-gray-400">Change your current password</p>
</div>
</Link>
<Link
to="/forgot-password"
className="flex items-center space-x-3 p-4 bg-gray-800 rounded-lg hover:bg-gray-700 transition-colors"
>
<FiMail className="w-5 h-5 text-indigo-500" />
<div>
<h3 className="text-white font-semibold">Forgot Password</h3>
<p className="text-sm text-gray-400">Request a password reset link</p>
</div>
</Link>
</div>
<div className="mt-8">
<div className="grid gap-6">
{Object.entries(documents).length > 0 ? (
@ -148,6 +193,7 @@ function Dashboard() {
</div>
</div>
</div>
</div>
);
}

View File

@ -1,88 +1,92 @@
import { useState } from 'react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { Switch } from '@headlessui/react'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
import { useState, useEffect } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
export default function Contact() {
const [agreed, setAgreed] = useState(false)
const navigate = useNavigate();
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [status, setStatus] = useState({ type: '', message: '' });
useEffect(() => {
window.scrollTo({ top: 0, behavior: "smooth" });
}, []);
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setStatus({ type: 'loading', message: 'Sending...' });
try {
await axios.post(`http://localhost:3000/email/contact`, formData);
setStatus({
type: 'success',
message: 'Thank you for your message. We will contact you soon.'
});
setFormData({ name: '', email: '', message: '' });
// Redirect to home page after 2 seconds
setTimeout(() => {
navigate('/');
}, 2000);
} catch (error) {
setStatus({
type: 'error',
message: 'There was an error sending your message. Please try again.'
});
}
};
return (
<div className="isolate bg-gray px-6 bg-gradient-to-b from-cyan-900 to-cyan-800 sm:py-32 lg:px-8">
<div
aria-hidden="true"
>
<div className="isolate bg-gray-900 px-6 py-24 sm:py-32 lg:px-8">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">Контактирајте нѐ</h2>
<p className="mt-2 text-lg leading-8 text-gray-300">
Имате прашања? Ние сме тука да помогнеме.
</p>
</div>
<div className="mx-auto max-w-2xl text-center mt-10">
<h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-3xl">Пишете ни порака | Закажете средба</h2>
{/* <p className="mt-2 text-lg leading-8 text-gray-600">
take a wheel or let it slide
</p> */}
</div>
<form action="https://formsubmit.co/taratur@gmail.com" method="POST" className="mx-auto mt-16 max-w-xl sm:mt-20">
<form onSubmit={handleSubmit} className="mx-auto mt-16 max-w-xl sm:mt-20">
<div className="grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2">
<div>
<label htmlFor="first-name" className="block text-sm font-semibold leading-6 text-white">
Име
</label>
<div className="mt-2.5">
<input
type="text"
name="first-name"
id="first-name"
autoComplete="given-name"
className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div>
<label htmlFor="last-name" className="block text-sm font-semibold leading-6 text-white">
Презиме
</label>
<div className="mt-2.5">
<input
type="text"
name="last-name"
id="last-name"
autoComplete="family-name"
className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div className="sm:col-span-2">
<label htmlFor="company" className="block text-sm font-semibold leading-6 text-white">
Компанија
<label htmlFor="name" className="block text-sm font-semibold leading-6 text-white">
Име и Презиме
</label>
<div className="mt-2.5">
<input
type="text"
name="company"
id="company"
autoComplete="organization"
className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="name"
id="name"
value={formData.name}
onChange={handleChange}
required
className="block w-full rounded-md border-0 bg-white/5 px-3.5 py-2 text-white shadow-sm ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div className="sm:col-span-2">
<label htmlFor="email" className="block text-sm font-semibold leading-6 text-white">
Мејл
Емаил
</label>
<div className="mt-2.5">
<input
type="email"
name="email"
id="email"
autoComplete="email"
className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
value={formData.email}
onChange={handleChange}
required
className="block w-full rounded-md border-0 bg-white/5 px-3.5 py-2 text-white shadow-sm ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm sm:leading-6"
/>
</div>
</div>
<div className="sm:col-span-2">
<label htmlFor="message" className="block text-sm font-semibold leading-6 text-white">
Порака
@ -92,24 +96,31 @@ export default function Contact() {
name="message"
id="message"
rows={4}
className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
defaultValue={''}
value={formData.message}
onChange={handleChange}
required
className="block w-full rounded-md border-0 bg-white/5 px-3.5 py-2 text-white shadow-sm ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm sm:leading-6"
/>
</div>
</div>
<Switch.Group as="div" className="flex gap-x-4 sm:col-span-2">
</Switch.Group>
</div>
<input type="hidden" name="_next" value="https://imk.mk/"></input>
{status.message && (
<div className={`mt-4 text-center ${status.type === 'error' ? 'text-red-500' : 'text-green-500'}`}>
{status.message}
</div>
)}
<div className="mt-10">
<button
type="submit"
className="block w-full rounded-md bg-gray-500 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-gray-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500"
disabled={status.type === 'loading'}
className="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
Испрати
{status.type === 'loading' ? 'Испраќање...' : 'Испрати Порака'}
</button>
</div>
</form>
</div>
)
);
}

2962
node_modules/.package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

2977
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
{
"dependencies": {
"@nestjs/cli": "^10.4.5",
"axios": "^1.7.7",
"date-fns": "^4.1.0",
"framer-motion": "^11.11.11",