diff --git a/backend/.env b/backend/.env index 91078d3..226bc31 100644 --- a/backend/.env +++ b/backend/.env @@ -16,7 +16,7 @@ AWS_ENDPOINT_URL=https://eu2.contabostorage.com # SMTP_HOST=smtp.gmail.com # SMTP_PORT=587 # SMTP_USER=taratur@gmail.com -# SMTP_PASS=dziy nccc svgg bovb +# SMTP_PASS=dziy nccc svgg bovb # EMAIL_FROM=taratur@gmail.com SMTP_HOST=imk.mk @@ -25,7 +25,13 @@ SMTP_USER=mailer@imk.mk SMTP_PASS=76Avtostoperski76 SMTP_FROM=mailer@imk.mk # FRONTEND_URL=https://imk.mk -# EMAIL_FROM=petrovskidimitar@yandex.com +EMAIL_FROM=mailer@yandex.com ADMIN_EMAIL=taratur@gmail.com + + +# default app ADMIN +DEFAULT_ADMIN_EMAIL=taratur@gmail.com +DEFAULT_ADMIN_PASSWORD=irina7654321 +DEFAULT_ADMIN_NAME=admin diff --git a/backend/README.md b/backend/README.md index 00a13b1..495c7f7 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,73 +1,47 @@ -

- Nest Logo -

+ Add the necessary environment variables to your `.env` file: -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest - -

A progressive Node.js framework for building efficient and scalable server-side applications.

-

-NPM Version -Package License -NPM Downloads -CircleCI -Coverage -Discord -Backers on Open Collective -Sponsors on Open Collective - - Support us - -

- - -## Description - -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. - -## Installation - -```bash -$ npm install +```env +# Default Admin Configuration +DEFAULT_ADMIN_EMAIL=admin@imk.com +DEFAULT_ADMIN_PASSWORD=admin123456 +DEFAULT_ADMIN_NAME=System Admin ``` -## Running the app +To use this setup: +1. The seed script will run automatically during deployment when you run: ```bash -# development -$ npm run start - -# watch mode -$ npm run start:dev - -# production mode -$ npm run start:prod +npx prisma db push +npx prisma db seed ``` -## Test - -```bash -# unit tests -$ npm run test - -# e2e tests -$ npm run test:e2e - -# test coverage -$ npm run test:cov +2. Alternatively, you can manually trigger the initialization by making a POST request to: +``` +POST /init/system ``` -## Support +This gives you two ways to ensure the default admin user is created: +1. Automatically during deployment via the seed script +2. Manually via the initialization endpoint -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). +To deploy, you would: -## Stay in touch +1. Set up your environment variables +2. Run the database migrations +3. Run the seed script +```bash +npm run prisma:deploy +npm run prisma:seed +``` -- Author - [Kamil Myƛliwiec](https://kamilmysliwiec.com) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) +The default admin credentials will be: +- Email: admin@imk.com (or whatever you set in env) +- Password: admin123456 (or whatever you set in env) -## License +Make sure to: +1. Change the default password after first login +2. Use strong passwords in production +3. Properly secure the initialization endpoint in production +4. Keep your environment variables secure -Nest is [MIT licensed](LICENSE). +This setup ensures you always have an admin user available after deployment while maintaining security and flexibility. diff --git a/backend/package.json b/backend/package.json index b58704f..cbf12a2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,7 +17,8 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "prisma:seed": "ts-node prisma/seed.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.679.0", diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index ddae7b9..fddd859 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -1,23 +1,47 @@ -import { PrismaClient } from '@prisma/client'; -import * as bcrypt from 'bcrypt'; +// backend/prisma/seed.ts +import { PrismaClient } from "@prisma/client"; +import * as bcrypt from "bcrypt"; const prisma = new PrismaClient(); async function main() { - const hashedPassword = await bcrypt.hash('admin123', 10); + const defaultAdminEmail = process.env.DEFAULT_ADMIN_EMAIL || "admin@imk.com"; + const defaultAdminPassword = + process.env.DEFAULT_ADMIN_PASSWORD || "admin123456"; + const defaultAdminName = process.env.DEFAULT_ADMIN_NAME || "System Admin"; - const admin = await prisma.user.upsert({ - where: { email: 'admin@example.com' }, - update: {}, - create: { - email: 'admin@example.com', - name: 'Admin User', - password: hashedPassword, - isAdmin: true, - }, - }); + try { + // Check if admin already exists + const existingAdmin = await prisma.user.findUnique({ + where: { email: defaultAdminEmail }, + }); - console.log({ admin }); + if (!existingAdmin) { + // Hash the password + const hashedPassword = await bcrypt.hash(defaultAdminPassword, 10); + + // Create the admin user + const admin = await prisma.user.create({ + data: { + email: defaultAdminEmail, + name: defaultAdminName, + password: hashedPassword, + isAdmin: true, + }, + }); + + console.log("Default admin user created:", { + id: admin.id, + email: admin.email, + name: admin.name, + }); + } else { + console.log("Default admin user already exists"); + } + } catch (error) { + console.error("Error creating default admin:", error); + throw error; + } } main() diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index f6d3754..751f184 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,39 +1,31 @@ -import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; -import { AuthModule } from './auth/auth.module'; +import { Module } from "@nestjs/common"; +import { AppController } from "./app.controller"; +import { AppService } from "./app.service"; +import { AuthModule } from "./auth/auth.module"; //import { TypeOrmModule } from '@nestjs/typeorm'; -import { AdminModule } from './admin/admin.module'; -import { ClientModule } from './client/client.module'; -import { UploadService } from './upload/upload.service'; -import { DocumentsService } from './documents/documents.service'; -import { S3Service } from './s3/s3.service'; -import { S3Module } from './s3/s3.module'; -import { PrismaService } from './prisma/prisma.service'; -import { PrismaModule } from './prisma/prisma.module'; -import { ConfigModule } from '@nestjs/config'; -import { AuthController } from './auth/auth.controller'; -import { DocumentsController } from './documents/documents.controller'; -import { JwtModule } from '@nestjs/jwt'; -import { EmailModule } from './email/email.module'; +import { AdminModule } from "./admin/admin.module"; +import { ClientModule } from "./client/client.module"; +import { UploadService } from "./upload/upload.service"; +import { DocumentsService } from "./documents/documents.service"; +import { S3Service } from "./s3/s3.service"; +import { S3Module } from "./s3/s3.module"; +import { PrismaService } from "./prisma/prisma.service"; +import { PrismaModule } from "./prisma/prisma.module"; +import { ConfigModule } from "@nestjs/config"; +import { AuthController } from "./auth/auth.controller"; +import { DocumentsController } from "./documents/documents.controller"; +import { JwtModule } from "@nestjs/jwt"; +import { EmailModule } from "./email/email.module"; +import { InitModule } from "./init/init.module"; @Module({ imports: [ - // TypeOrmModule.forRoot({ - // type: 'postgres', - // host: 'localhost', - // port: 5432, - // username: 'root', - // password: 'admin', - // database: 'imk', - // synchronize: true, - // }), ConfigModule.forRoot({ isGlobal: true, }), JwtModule.register({ secret: process.env.JWT_SECRET, - signOptions: { expiresIn: '1h' }, + signOptions: { expiresIn: "1h" }, }), AuthModule, AdminModule, @@ -41,6 +33,7 @@ import { EmailModule } from './email/email.module'; S3Module, PrismaModule, EmailModule, + InitModule, ], controllers: [AppController, AuthController, DocumentsController], providers: [ diff --git a/backend/src/email/email.service.ts b/backend/src/email/email.service.ts index 993c3de..a3cb526 100644 --- a/backend/src/email/email.service.ts +++ b/backend/src/email/email.service.ts @@ -1,6 +1,6 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import * as nodemailer from 'nodemailer'; +import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import * as nodemailer from "nodemailer"; @Injectable() export class EmailService { @@ -9,29 +9,26 @@ export class EmailService { private readonly from: string; constructor(private configService: ConfigService) { - console.log('Initializing EmailService...'); // Direct console log for debugging - this.logger.log('Initializing EmailService...'); + 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'); + 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 }); + 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 host: host, port: port, auth: { user, pass }, - debug: true, // Enable debug logs - logger: true // Enable transport level logging + debug: true, // Enable debug logs + logger: true, // Enable transport level logging }); // Verify connection @@ -40,16 +37,16 @@ export class EmailService { private async verifyConnection() { try { - console.log('Verifying SMTP connection...'); // Direct console log - this.logger.log('Verifying SMTP connection...'); - + 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); + + 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:', { + console.error("SMTP connection failed:", error); // Direct console log + this.logger.error("SMTP connection failed:", { error: error.message, code: error.code, command: error.command, @@ -68,7 +65,7 @@ export class EmailService { const mailOptions = { from: `"IMK Platform" <${this.from}>`, to: userEmail, - subject: 'Welcome to IMK Platform!', + subject: "Welcome to IMK Platform!", html: `

Welcome to IMK Platform!

@@ -78,21 +75,21 @@ export class EmailService {

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:', { + 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 + 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:', { + + console.log("Email sent successfully:", info); // Direct console log + this.logger.log("Email sent successfully:", { messageId: info.messageId, response: info.response, accepted: info.accepted, @@ -100,8 +97,8 @@ export class EmailService { envelope: info.envelope, }); } catch (error) { - console.error('Failed to send email:', error); // Direct console log - this.logger.error('Failed to send email:', { + 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, @@ -117,14 +114,18 @@ export class EmailService { userEmail: string, username: string, documentName: string, - action: 'uploaded' | 'shared', + 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'; + 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, @@ -142,31 +143,31 @@ export class EmailService { }; try { - console.log('Sending document notification email with options:', { + console.log("Sending document notification email with options:", { to: mailOptions.to, from: mailOptions.from, subject: mailOptions.subject, documentName, - action + action, }); - this.logger.log('Sending document notification email with options:', { + this.logger.log("Sending document notification email with options:", { to: mailOptions.to, from: mailOptions.from, subject: mailOptions.subject, documentName, - action + action, }); const info = await this.transporter.sendMail(mailOptions); - - console.log('Document notification email sent successfully:', { + + 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:', { + this.logger.log("Document notification email sent successfully:", { messageId: info.messageId, response: info.response, accepted: info.accepted, @@ -174,8 +175,8 @@ export class EmailService { envelope: info.envelope, }); } catch (error) { - console.error('Failed to send document notification email:', error); - this.logger.error('Failed to send document notification email:', { + 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, @@ -188,12 +189,12 @@ export class EmailService { } async sendPasswordResetNotification(email: string, name: string) { - this.logger.log('Sending password reset notification email to:', email); + this.logger.log("Sending password reset notification email to:", email); const mailOptions = { from: this.from, to: email, - subject: 'Your Password Has Been Reset', + subject: "Your Password Has Been Reset", html: `

Password Reset Notification

@@ -206,12 +207,15 @@ export class EmailService { }; try { - this.logger.debug('Attempting to send password reset notification...'); + this.logger.debug("Attempting to send password reset notification..."); const info = await this.transporter.sendMail(mailOptions); - this.logger.log('Password reset notification sent successfully:', info.response); + this.logger.log( + "Password reset notification sent successfully:", + info.response, + ); return info; } catch (error) { - this.logger.error('Failed to send password reset notification:', { + this.logger.error("Failed to send password reset notification:", { error: error.message, code: error.code, command: error.command, @@ -222,13 +226,13 @@ export class EmailService { } async sendPasswordResetEmail(email: string, name: string, token: string) { - this.logger.log('Sending password reset email to:', email); + this.logger.log("Sending password reset email to:", email); const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${token}`; const mailOptions = { from: this.from, to: email, - subject: 'Reset Your Password - IMK Platform', + subject: "Reset Your Password - IMK Platform", html: `

Password Reset Request

@@ -252,12 +256,12 @@ export class EmailService { }; try { - this.logger.debug('Attempting to send password reset email...'); + this.logger.debug("Attempting to send password reset email..."); const info = await this.transporter.sendMail(mailOptions); - this.logger.log('Password reset email sent successfully:', info.response); + this.logger.log("Password reset email sent successfully:", info.response); return info; } catch (error) { - this.logger.error('Failed to send password reset email:', { + this.logger.error("Failed to send password reset email:", { error: error.message, code: error.code, command: error.command, @@ -268,12 +272,12 @@ export class EmailService { } async sendPasswordChangeConfirmation(email: string, name: string) { - this.logger.log('Sending password change confirmation to:', email); + this.logger.log("Sending password change confirmation to:", email); const mailOptions = { from: this.from, to: email, - subject: 'Password Changed Successfully - IMK Platform', + subject: "Password Changed Successfully - IMK Platform", html: `

Password Changed Successfully

@@ -286,12 +290,15 @@ export class EmailService { }; try { - this.logger.debug('Attempting to send password change confirmation...'); + this.logger.debug("Attempting to send password change confirmation..."); const info = await this.transporter.sendMail(mailOptions); - this.logger.log('Password change confirmation sent successfully:', info.response); + this.logger.log( + "Password change confirmation sent successfully:", + info.response, + ); return info; } catch (error) { - this.logger.error('Failed to send password change confirmation:', { + this.logger.error("Failed to send password change confirmation:", { error: error.message, code: error.code, command: error.command, @@ -300,4 +307,4 @@ export class EmailService { throw error; } } -} \ No newline at end of file +} diff --git a/backend/src/init/init.controller.spec.ts b/backend/src/init/init.controller.spec.ts new file mode 100644 index 0000000..7b6abb8 --- /dev/null +++ b/backend/src/init/init.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { InitController } from './init.controller'; + +describe('InitController', () => { + let controller: InitController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [InitController], + }).compile(); + + controller = module.get(InitController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/init/init.controller.ts b/backend/src/init/init.controller.ts new file mode 100644 index 0000000..0c7504e --- /dev/null +++ b/backend/src/init/init.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Post, Logger } from "@nestjs/common"; +import { InitService } from "./init.service"; + +@Controller("init") +export class InitController { + private readonly logger = new Logger(InitController.name); + + constructor(private readonly initService: InitService) {} + + @Post("system") + async initializeSystem() { + this.logger.log("Received system initialization request"); + return this.initService.initializeSystem(); + } +} diff --git a/backend/src/init/init.module.ts b/backend/src/init/init.module.ts new file mode 100644 index 0000000..1d5909e --- /dev/null +++ b/backend/src/init/init.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { InitController } from "./init.controller"; +import { PrismaModule } from "../prisma/prisma.module"; +import { ConfigModule } from "@nestjs/config"; +import { InitService } from "./init.service"; + +@Module({ + imports: [PrismaModule, ConfigModule], + providers: [InitService], + controllers: [InitController], +}) +export class InitModule {} diff --git a/backend/src/init/init.service.spec.ts b/backend/src/init/init.service.spec.ts new file mode 100644 index 0000000..b9a567b --- /dev/null +++ b/backend/src/init/init.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { InitService } from './init.service'; + +describe('InitService', () => { + let service: InitService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [InitService], + }).compile(); + + service = module.get(InitService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/init/init.service.ts b/backend/src/init/init.service.ts new file mode 100644 index 0000000..3138e0f --- /dev/null +++ b/backend/src/init/init.service.ts @@ -0,0 +1,71 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { ConfigService } from "@nestjs/config"; +import * as bcrypt from "bcrypt"; + +@Injectable() +export class InitService { + private readonly logger = new Logger(InitService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly configService: ConfigService, + ) {} + + async initializeSystem() { + this.logger.log("Starting system initialization..."); + + const defaultAdminEmail = + this.configService.get("DEFAULT_ADMIN_EMAIL") || "taratur@gmail"; + const defaultAdminPassword = + this.configService.get("DEFAULT_ADMIN_PASSWORD") || "irina7654321"; + const defaultAdminName = + this.configService.get("DEFAULT_ADMIN_NAME") || "System Admin"; + + try { + // Check if admin already exists + const existingAdmin = await this.prisma.user.findUnique({ + where: { email: defaultAdminEmail }, + }); + + if (!existingAdmin) { + // Hash the password + const hashedPassword = await bcrypt.hash(defaultAdminPassword, 10); + + // Create the admin user + const admin = await this.prisma.user.create({ + data: { + email: defaultAdminEmail, + name: defaultAdminName, + password: hashedPassword, + isAdmin: true, + }, + }); + + this.logger.log("Default admin user created successfully"); + return { + success: true, + message: "System initialized successfully", + admin: { + id: admin.id, + email: admin.email, + name: admin.name, + }, + }; + } + + return { + success: true, + message: "System already initialized", + admin: { + id: existingAdmin.id, + email: existingAdmin.email, + name: existingAdmin.name, + }, + }; + } catch (error) { + this.logger.error("Error during system initialization:", error); + throw error; + } + } +}