docker-compose up and running :)

This commit is contained in:
dimitar 2025-03-31 04:40:31 +02:00
parent 86254bf7eb
commit d9f9aaedc5
25 changed files with 4194 additions and 1988 deletions

38
.env Normal file
View File

@ -0,0 +1,38 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
POSTGRES_USER=root
POSTGRES_PASSWORD=irina76
POSTGRES_DB=imk_db
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/imk_db?schema=public"
#DATABASE_URL="postgresql://root:irina76@localhost:5432/imk?schema=public"
JWT_SECRET=some-secret
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
#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_PASS=76Avtostoperski76
SMTP_FROM=mailer@imk.mk
# FRONTEND_URL=https://imk.mk
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

2
.gitignore vendored
View File

@ -7,4 +7,6 @@ frontend/node_modules
frontend/dist
frontend/.vite
node_modules
pestgres_data/
redis_data/

35
_.env Normal file
View File

@ -0,0 +1,35 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://root:irina76@localhost:5433/imk2?schema=public"
JWT_SECRET=some-secret
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
#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_PASS=76Avtostoperski76
SMTP_FROM=mailer@imk.mk
# FRONTEND_URL=https://imk.mk
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

108
_docker-compose.yml Normal file
View File

@ -0,0 +1,108 @@
version: "3.8"
services:
postgres:
image: postgres:15-alpine
container_name: imk-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: imk_db
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- imk_copy_imk_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: imk-backend
# environment:
# - NODE_ENV=development
# - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/imk_db
# - AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6
# - AWS_ENDPOINT_URL=https://eu2.contabostorage.com
# - AWS_REGION=EU2
# - AWS_S3_BUCKET_NAME=imk-data
# - AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4
# - DEFAULT_ADMIN_EMAIL=taratur@gmail.com
# - DEFAULT_ADMIN_NAME=admin
# - DEFAULT_ADMIN_PASSWORD=irina7654321
# - EMAIL_FROM=mailer@yandex.com
# - JWT_SECRET=some-secret
# - PORT=3000
# - SMTP_HOST=imk.mk
# - SMTP_PASS=76Avtostoperski76
# - SMTP_PORT=465
# - SMTP_USER=mailer@imk.mk
# ports:
# - "3000:3000"
# depends_on:
# postgres:
# condition: service_healthy
ports:
- "3000:3000"
environment:
- NODE_ENV=production
# - PORT=3000
- DATABASE_URL=postgresql://postgres:postgres@imk-postgres:5432/postgres?schema=public
- AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6
- AWS_ENDPOINT_URL=https://eu2.contabostorage.com
- AWS_REGION=EU2
- AWS_S3_BUCKET_NAME=imk-data
- AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4
- DEFAULT_ADMIN_EMAIL=taratur@gmail.com
- DEFAULT_ADMIN_NAME=admin
- DEFAULT_ADMIN_PASSWORD=irina7654321
- EMAIL_FROM=mailer@yandex.com
- JWT_SECRET=some-secret
- PORT=3000
- SMTP_HOST=imk.mk
- SMTP_PASS=76Avtostoperski76
- SMTP_PORT=465
- SMTP_USER=mailer@imk.mk
# env_file:
# - .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
networks:
- imk_copy_imk_network
volumes:
- ./backend:/usr/src/app
- /usr/src/app/node_modules
command: sh -c "npm run prisma:generate && npm run prisma:migrate:deploy && npm run start:dev"
redis:
image: redis:alpine
container_name: imk-redis
ports:
- "6379:6379"
networks:
- imk_copy_imk_network
volumes:
- redis_data:/data
networks:
imk_copy_imk_network:
driver: bridge
volumes:
postgres_data:
redis_data:

View File

@ -3,8 +3,11 @@
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://root:irina76@localhost:5432/imk?schema=public"
POSTGRES_USER=root
POSTGRES_PASSWORD=irina76
POSTGRES_DB=imk_db
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/imk_db?schema=public"
#DATABASE_URL="postgresql://root:irina76@localhost:5432/imk?schema=public"
JWT_SECRET=some-secret
AWS_REGION=EU2
AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6
@ -27,10 +30,8 @@ SMTP_FROM=mailer@imk.mk
# FRONTEND_URL=https://imk.mk
EMAIL_FROM=mailer@yandex.com
ADMIN_EMAIL=taratur@gmail.com
# default app ADMIN
DEFAULT_ADMIN_EMAIL=taratur@gmail.com
DEFAULT_ADMIN_PASSWORD=irina7654321

42
backend/Dockerfile Normal file
View File

@ -0,0 +1,42 @@
FROM node:18.19.1-alpine3.18
WORKDIR /usr/src/app
# Install necessary tools and dependencies
RUN apk add --no-cache \
curl \
wget \
python3 \
make \
g++ \
&& rm -rf /var/cache/apk/*
# Copy package files
COPY package*.json ./
COPY prisma ./prisma/
# Install dependencies
# RUN npm ci --only=production
# Generate Prisma client
RUN npx prisma generate
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Expose port
EXPOSE 3000
# Add healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget -q --spider http://localhost:3000/health || exit 1
# Set Node options
ENV NODE_OPTIONS="--max-old-space-size=2048"
# Start the application directly with node
CMD ["node", "dist/main.js"]

4867
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,19 +6,22 @@
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"start:prod": "node dist/main.js",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"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",
"prisma:seed": "ts-node prisma/seed.ts"
"prisma:generate": "prisma generate",
"prisma:migrate:dev": "prisma migrate dev",
"prisma:migrate:deploy": "prisma migrate deploy"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.679.0",
@ -32,7 +35,7 @@
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.6",
"@nestjs/typeorm": "^10.0.2",
"@prisma/client": "^5.12.1",
"@prisma/client": "^6.5.0",
"@types/multer": "^1.4.12",
"@types/nodemailer": "^6.4.17",
"bcrypt": "^5.1.1",
@ -44,8 +47,8 @@
"passport-local": "^1.0.0",
"pg": "^8.13.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20"
"rimraf": "^5.0.0",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
@ -65,7 +68,7 @@
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"prisma": "^5.12.1",
"prisma": "^6.5.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
@ -74,9 +77,7 @@
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
"prisma": {},
"jest": {
"moduleFileExtensions": [
"js",

View File

@ -1,82 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
var client_1 = require("@prisma/client");
var bcrypt = require("bcrypt");
var prisma = new client_1.PrismaClient();
function main() {
return __awaiter(this, void 0, void 0, function () {
var hashedPassword, admin;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, bcrypt.hash('admin123', 10)];
case 1:
hashedPassword = _a.sent();
return [4 /*yield*/, prisma.user.upsert({
where: { email: 'admin@example.com' },
update: {},
create: {
email: 'admin@example.com',
name: 'Admin User',
password: hashedPassword,
isAdmin: true,
},
})];
case 2:
admin = _a.sent();
console.log({ admin: admin });
return [2 /*return*/];
}
});
});
}
main()
.catch(function (e) {
console.error(e);
process.exit(1);
})
.finally(function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, prisma.$disconnect()];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); });

View File

@ -1,54 +0,0 @@
// backend/prisma/seed.ts
import { PrismaClient } from "@prisma/client";
import * as bcrypt from "bcrypt";
const prisma = new PrismaClient();
async function main() {
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";
try {
// Check if admin already exists
const existingAdmin = await 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 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()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@ -17,6 +17,7 @@ import { DocumentsController } from "./documents/documents.controller";
import { JwtModule } from "@nestjs/jwt";
import { EmailModule } from "./email/email.module";
import { InitModule } from "./init/init.module";
import { HealthController } from './health/health.controller';
@Module({
imports: [
@ -35,7 +36,7 @@ import { InitModule } from "./init/init.module";
EmailModule,
InitModule,
],
controllers: [AppController, AuthController, DocumentsController],
controllers: [AppController, AuthController, DocumentsController, HealthController],
providers: [
AppService,
UploadService,

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HealthController } from './health.controller';
describe('HealthController', () => {
let controller: HealthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HealthController],
}).compile();
controller = module.get<HealthController>(HealthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,9 @@
import { Controller, Get } from "@nestjs/common";
@Controller("health")
export class HealthController {
@Get()
check() {
return { status: "ok" };
}
}

View File

@ -8,5 +8,6 @@ import { InitService } from "./init.service";
imports: [PrismaModule, ConfigModule],
providers: [InitService],
controllers: [InitController],
exports: [InitService],
})
export class InitModule {}

View File

@ -1,10 +1,10 @@
import { Injectable, Logger } from "@nestjs/common";
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { ConfigService } from "@nestjs/config";
import * as bcrypt from "bcrypt";
@Injectable()
export class InitService {
export class InitService implements OnModuleInit {
private readonly logger = new Logger(InitService.name);
constructor(
@ -12,23 +12,30 @@ export class InitService {
private readonly configService: ConfigService,
) {}
async onModuleInit() {
this.logger.log("Initializing application and creating default admin...");
await this.initializeSystem();
}
async initializeSystem() {
this.logger.log("Starting system initialization...");
const defaultAdminEmail =
this.configService.get("DEFAULT_ADMIN_EMAIL") || "taratur@gmail";
this.configService.get("DEFAULT_ADMIN_EMAIL") || "taratur@gmail.com";
const defaultAdminPassword =
this.configService.get("DEFAULT_ADMIN_PASSWORD") || "irina7654321";
const defaultAdminName =
this.configService.get("DEFAULT_ADMIN_NAME") || "System Admin";
try {
this.logger.log("Checking for existing admin user...");
// Check if admin already exists
const existingAdmin = await this.prisma.user.findUnique({
where: { email: defaultAdminEmail },
});
if (!existingAdmin) {
this.logger.log("No admin found. Creating default admin user...");
// Hash the password
const hashedPassword = await bcrypt.hash(defaultAdminPassword, 10);
@ -42,7 +49,12 @@ export class InitService {
},
});
this.logger.log("Default admin user created successfully");
this.logger.log("Default admin user created successfully", {
id: admin.id,
email: admin.email,
name: admin.name,
});
return {
success: true,
message: "System initialized successfully",
@ -54,6 +66,12 @@ export class InitService {
};
}
this.logger.log("Default admin user already exists", {
id: existingAdmin.id,
email: existingAdmin.email,
name: existingAdmin.name,
});
return {
success: true,
message: "System already initialized",

View File

@ -1,46 +1,43 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger, ValidationPipe } from '@nestjs/common';
// src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { Logger, ValidationPipe } from "@nestjs/common";
async function bootstrap() {
const logger = new Logger('Bootstrap');
logger.log('Starting application...');
const logger = new Logger("Bootstrap");
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log', 'debug', 'verbose'], // Enable all log levels
});
logger.log('Configuring application...');
try {
logger.log("Starting 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,
},
}));
const app = await NestFactory.create(AppModule, {
logger: ["error", "warn", "log", "debug", "verbose"],
});
// 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'],
});
// Enable CORS
app.enableCors({
origin: true,
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
credentials: true,
});
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()}`);
// Global pipes
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
}),
);
const port = process.env.PORT || 3000;
logger.log(`Attempting to start server on port ${port}...`);
await app.listen(port, "0.0.0.0");
logger.log(`Application is running on: ${await app.getUrl()}`);
} catch (error) {
logger.error("Failed to start application:", error);
process.exit(1);
}
}
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
bootstrap();

15
backend/temp.sql Normal file
View File

@ -0,0 +1,15 @@
INSERT INTO "User" (
email,
name,
password,
"isAdmin",
"createdAt",
"updatedAt"
) VALUES (
'taratur@gmail.com',
'admin',
'$2b$10$b/cbgtQqMp.a.mbhQoyPa.60GyVGJDpW3jHesWHZtmL7jZr2cpXxm',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);

91
docker-compose.yml Normal file
View File

@ -0,0 +1,91 @@
version: "3.8"
services:
backend:
container_name: imk-backend
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- DATABASE_URL=postgresql://postgres:postgres@imk-postgres:5432/postgres?schema=public
env_file:
- .env
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 512M
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- app_network
healthcheck:
test:
[
"CMD",
"wget",
"-q",
"--spider",
"http://localhost:3000/health || exit 1",
]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
restart: unless-stopped
postgres:
container_name: imk-postgres
image: postgres:14-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app_network
restart: unless-stopped
redis:
container_name: imk-redis
image: redis:alpine
command: redis-server --appendonly yes
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app_network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
restart: unless-stopped
networks:
app_network:
driver: bridge
name: app_network
volumes:
postgres_data:
name: imk_postgres_data
redis_data:
name: imk_redis_data

14
docker-reset.md Normal file
View File

@ -0,0 +1,14 @@
# Stop everything
docker-compose down
# Remove all containers
docker rm -f $(docker ps -a -q)
# Remove all volumes
docker volume rm $(docker volume ls -q)
# Remove all images
docker rmi -f $(docker images -q)
# Start fresh
docker-compose up --build

253
frontend/documentation.md Normal file
View File

@ -0,0 +1,253 @@
# IMK Platform Documentation
## Table of Contents
1. [Introduction](#introduction)
2. [Features](#features)
3. [Technical Stack](#technical-stack)
4. [Architecture](#architecture)
5. [User Flows](#user-flows)
6. [API Documentation](#api-documentation)
7. [Security](#security)
8. [Deployment](#deployment)
## Introduction
IMK Platform is a modern web application built to manage and share documents securely. The platform provides robust user management, document handling, and secure sharing capabilities.
## Features
### User Management
- User registration and authentication
- Role-based access control (Admin, User)
- Password reset functionality
- Email notifications for account activities
- Profile management
### Document Management
- Document upload and storage
- Document sharing between users
- Document version control
- Document metadata management
- Secure document access control
### Email Notifications
- Welcome emails for new users
- Password reset notifications
- Document sharing notifications
- Password change confirmations
### Administrative Features
- User management dashboard
- Document oversight
- System monitoring
- Access control management
## Technical Stack
### Frontend
- React.js with Vite
- TypeScript for type safety
- TailwindCSS for styling
- Shadcn UI components
- React Query for state management
- React Router for navigation
### Backend
- NestJS framework
- TypeScript
- Prisma ORM
- PostgreSQL database
- Node.js runtime
- JWT authentication
- Nodemailer for email services
## Architecture
### Frontend Architecture
- Component-based architecture
- Responsive design
- State management using React Query
- Protected routes with authentication
- Form validation and error handling
### Backend Architecture
- RESTful API design
- Modular architecture with NestJS
- Database abstraction with Prisma
- Email service integration
- JWT-based authentication
- Role-based authorization
## User Flows
### Authentication Flow
1. User Registration
- User fills registration form
- System validates input
- Welcome email sent
- User redirected to login
2. Login Flow
- User enters credentials
- System validates credentials
- JWT token issued
- User redirected to dashboard
3. Password Reset Flow
- User requests password reset
- System sends reset email
- User clicks reset link
- User sets new password
- Confirmation email sent
### Document Management Flow
1. Document Upload
- User selects document
- System validates document
- Document metadata captured
- Document stored securely
2. Document Sharing
- User selects document to share
- User selects recipient(s)
- System sends notification
- Access granted to recipient
## API Documentation
### Authentication Endpoints
- POST /auth/register - User registration
- POST /auth/login - User login
- POST /auth/reset-password - Password reset request
- POST /auth/change-password - Password change
### User Endpoints
- GET /users/profile - Get user profile
- PUT /users/profile - Update user profile
- GET /users - List users (admin only)
- PUT /users/:id - Update user (admin only)
### Document Endpoints
- POST /documents - Upload document
- GET /documents - List documents
- GET /documents/:id - Get document details
- PUT /documents/:id - Update document
- DELETE /documents/:id - Delete document
- POST /documents/:id/share - Share document
## Security
### Authentication Security
- JWT token-based authentication
- Password hashing with bcrypt
- Rate limiting on auth endpoints
- Session management
- CSRF protection
### Data Security
- HTTPS encryption
- Input validation
- XSS protection
- SQL injection prevention
- File type validation
### Access Control
- Role-based access control
- Document-level permissions
- API endpoint protection
- Resource isolation
## Deployment
### Frontend Deployment
- Static site hosting
- CDN integration
- Environment configuration
- Build optimization
### Backend Deployment
- Node.js runtime environment
- Process management with PM2
- Nginx reverse proxy
- SSL/TLS configuration
- Database backup system
### Environment Variables
Frontend:
```env
VITE_API_URL=https://api.example.com
```
Backend:
```env
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=user@example.com
SMTP_PASS=password
EMAIL_FROM=noreply@example.com
JWT_SECRET=your-secret-key
```
## Getting Started
### Development Setup
1. Clone the repository
2. Install dependencies:
```bash
# Frontend
cd frontend
npm install
# Backend
cd backend
npm install
```
3. Set up environment variables
4. Start development servers:
```bash
# Frontend
npm run dev
# Backend
npm run start:dev
```
### Production Deployment
1. Build applications:
```bash
# Frontend
npm run build
# Backend
npm run build
```
2. Configure environment variables
3. Start production servers:
```bash
# Frontend
serve -s dist
# Backend
npm run start:prod
```
## Support and Maintenance
### Monitoring
- Application performance monitoring
- Error tracking and logging
- Database monitoring
- Email service monitoring
### Backup and Recovery
- Database backup strategy
- Document backup system
- System configuration backup
- Recovery procedures
### Updates and Maintenance
- Regular security updates
- Dependency updates
- Performance optimization
- Feature updates

View File

@ -1,5 +1,5 @@
import { createContext, useContext, useState, useEffect } from 'react';
import api from '../services/api';
import { createContext, useContext, useState, useEffect } from "react";
import api from "../services/api";
const AuthContext = createContext(null);
@ -10,17 +10,17 @@ export const AuthProvider = ({ children }) => {
useEffect(() => {
const fetchUser = async () => {
try {
const token = localStorage.getItem('token');
const token = localStorage.getItem("token");
if (!token) {
setIsLoading(false);
return;
}
const response = await api.get('/auth/user-info'); // Updated endpoint
const response = await api.get("/auth/user-info"); // Updated endpoint
setUser(response.data);
} catch (error) {
console.error('Failed to fetch user info:', error);
localStorage.removeItem('token');
console.error("Failed to fetch user info:", error);
localStorage.removeItem("token");
} finally {
setIsLoading(false);
}
@ -28,44 +28,29 @@ export const AuthProvider = ({ children }) => {
fetchUser();
}, []);
// const login = async (username, password) => {
// try {
// const response = await api.post('/auth/login', { username, password });
// const { access_token, user } = response.data; // Make sure this matches your backend response
// localStorage.setItem('token', access_token);
// setUser(user);
// return user;
// } catch (error) {
// console.error('Login error:', error);
// throw error;
// }
// };
const login = async (username, password) => {
try {
const response = await api.post('/auth/login', { username, password });
console.log('Login response:', response.data); // Debug log
const response = await api.post("/auth/login", { username, password });
console.log("Login response:", response.data); // Debug log
const { access_token } = response.data;
localStorage.setItem('token', access_token);
localStorage.setItem("token", access_token);
// Fetch user info after login
const userResponse = await api.get('/auth/user-info');
const userResponse = await api.get("/auth/user-info");
const userData = userResponse.data;
setUser(userData);
return userData; // Return the user data for redirect logic
} catch (error) {
console.error('Login error:', error);
console.error("Login error:", error);
throw error;
}
};
const logout = () => {
localStorage.removeItem('token');
localStorage.removeItem("token");
setUser(null);
};
@ -79,7 +64,7 @@ export const AuthProvider = ({ children }) => {
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
};

View File

@ -0,0 +1,261 @@
import axios from "axios";
// Create axios instance with default config
const api = axios.create({
baseURL: "http://localhost:3000",
withCredentials: true,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
// Authentication endpoints
export const login = async (credentials) => {
try {
const response = await api.post(
"/auth/login",
{
email: credentials.email,
password: credentials.password,
},
{
headers: {
Accept: "application/json",
},
},
);
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const register = async (userData) => {
try {
const response = await api.post("/auth/register", userData);
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const logout = async () => {
try {
const response = await api.post("/auth/logout");
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
// Document endpoints
export const createDocument = async (documentData) => {
try {
const response = await api.post("/documents", documentData);
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const getDocument = async (documentId) => {
try {
const response = await api.get(`/documents/${documentId}`);
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const updateDocument = async (documentId, documentData) => {
try {
const response = await api.put(`/documents/${documentId}`, documentData);
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const deleteDocument = async (documentId) => {
try {
const response = await api.delete(`/documents/${documentId}`);
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const getAllDocuments = async () => {
try {
const response = await api.get("/documents");
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const shareDocument = async (documentId, userData) => {
try {
const response = await api.post(`/documents/${documentId}/share`, userData);
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const getSharedDocuments = async () => {
try {
const response = await api.get("/documents/shared");
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const uploadDocument = async (file) => {
try {
const formData = new FormData();
formData.append("file", file);
const response = await api.post("/documents/upload", formData);
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const getUserInfo = async () => {
try {
const response = await api.get("/users/me");
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const getAllUsers = async () => {
try {
const response = await api.get("/users");
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const createUser = async (user) => {
try {
const response = await api.post("/users", user);
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const resetUserPassword = async (userId, password) => {
try {
const response = await api.put(`admin/users/${userId}/reset-password`, {
password,
});
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const downloadDocument = async (documentId) => {
try {
const response = await api.get(`/documents/${documentId}/download`, {
responseType: "blob",
});
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const resetPassword = async (userId, password) => {
try {
const response = await api.put("/auth/reset-password", {
password,
});
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
export const forgotPassword = async (email) => {
try {
const response = await api.post("/auth/forgot-password", {
email,
});
return response.data;
} catch (error) {
if (error.response) {
throw error.response.data;
}
throw error;
}
};
// Add authorization header to requests if token exists
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
export default api;

View File

@ -1,107 +1,116 @@
import axios from 'axios';
import axios from "axios";
const API_URL = 'http://localhost:3000';
const API_URL = "http://localhost:3000";
const api = axios.create({
baseURL: API_URL,
// withCredentials: true,
withCredentials: true,
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
},
});
// Add authorization header to all requests
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
const token = localStorage.getItem("token");
if (token) {
config.headers['Authorization'] = `Bearer ${token}`; // Make sure this matches your backend expectation
config.headers["Authorization"] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
},
);
export const getSharedDocuments = (userId) => {
return api.get(`/documents/shared/${userId}`);
};
const getToken = () => localStorage.getItem('token');
const getToken = () => localStorage.getItem("token");
export const downloadDocument = async (key) => {
try {
const response = await api.get(`/documents/download/${encodeURIComponent(key)}`, {
responseType: 'blob',
headers: {
'Accept': 'application/octet-stream',
}
});
const response = await api.get(
`/documents/download/${encodeURIComponent(key)}`,
{
responseType: "blob",
headers: {
Accept: "application/octet-stream",
},
},
);
// Create blob URL and trigger download
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
// Extract filename from key
const fileName = key.split('/').pop();
const fileName = key.split("/").pop();
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
return response;
} catch (error) {
console.error('Download error:', error);
console.error("Download error:", error);
throw error;
}
};
export const createUser = (userData) => {
return api.post('/admin/users', {
return api.post("/admin/users", {
name: userData.name,
email: userData.email,
password: userData.password,
isAdmin: userData.isAdmin
isAdmin: userData.isAdmin,
});
};
export const login = (username, password) => api.post('/auth/login', { username, password });
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 login = (username, password) =>
api.post("/auth/login", { username, password });
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) => {
try {
// Debug log
console.log('Sending to server:', {
title: formData.get('title'),
sharedWithId: formData.get('sharedWithId'),
uploadedById: formData.get('uploadedById')
console.log("Sending to server:", {
title: formData.get("title"),
sharedWithId: formData.get("sharedWithId"),
uploadedById: formData.get("uploadedById"),
});
const response = await api.post('/admin/documents', formData, {
const response = await api.post("/admin/documents", formData, {
headers: {
'Content-Type': 'multipart/form-data',
"Content-Type": "multipart/form-data",
},
});
return response.data;
} catch (error) {
console.error('API Error:', error.response?.data);
console.error("API Error:", error.response?.data);
throw error;
}
};
export const getUserInfo = () => api.get('/auth/user-info');
export const getUserInfo = () => api.get("/auth/user-info");
export const getAllDocuments = () => api.get('/admin/documents');
export const getAllDocuments = () => api.get("/admin/documents");
// export const getSharedDocuments = (userId) => api.get(`/documents/shared/${userId}`);
export const getAllUsers = () => api.get('/admin/users');
export const resetUserPassword = (userId, newPassword) => api.post(`/admin/users/${userId}/reset-password`, { password: newPassword });
export const getAllUsers = () => api.get("/admin/users");
export const resetUserPassword = (userId, newPassword) =>
api.post(`/admin/users/${userId}/reset-password`, { password: newPassword });
// Password reset endpoints
export const forgotPassword = (email) => api.post('/auth/forgot-password', { email });
export const resetPassword = (token, newPassword) => api.post('/auth/reset-password', { token, newPassword });
export const forgotPassword = (email) =>
api.post("/auth/forgot-password", { email });
export const resetPassword = (token, newPassword) =>
api.post("/auth/reset-password", { token, newPassword });
export default api;

View File

@ -1,7 +1,17 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
server: {
port: 5173,
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
secure: false,
},
},
},
});

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "imk_copy",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}