This commit is contained in:
Dimitar765 2024-10-28 18:43:22 +01:00
parent 2b9dad1fbf
commit 8cbdde39c1
139 changed files with 19353 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
application/cache
backend/imk-backend/node_modules
backend/imk-backend/dist
backend/imk-backend/test
frontend/imk/node_modules
frontend/imk/dist

12
backend/imk-backend/.env Normal file
View File

@ -0,0 +1,12 @@
# 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:admin@localhost:5432/imk?schema=public"
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

View File

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
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).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

11348
backend/imk-backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,91 @@
{
"name": "imk-backend",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"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",
"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"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.679.0",
"@aws-sdk/lib-storage": "^3.679.0",
"@aws-sdk/s3-request-presigner": "^3.679.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "^2.0.5",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.6",
"@nestjs/typeorm": "^10.0.2",
"@prisma/client": "^5.21.1",
"@types/multer": "^1.4.12",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.13.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@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/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"prisma": "^5.21.1",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -0,0 +1,27 @@
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"password" TEXT NOT NULL,
"isAdmin" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Document" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"authorId" INTEGER NOT NULL,
CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Document" ADD CONSTRAINT "Document_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Document" ADD COLUMN "s3Key" TEXT;

View File

@ -0,0 +1,8 @@
/*
Warnings:
- Made the column `s3Key` on table `Document` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "Document" ALTER COLUMN "s3Key" SET NOT NULL;

View File

@ -0,0 +1,42 @@
/*
Warnings:
- Added the required column `updatedAt` to the `Document` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Document" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "status" TEXT NOT NULL DEFAULT 'pending',
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
-- CreateTable
CREATE TABLE "Notification" (
"id" SERIAL NOT NULL,
"message" TEXT NOT NULL,
"read" BOOLEAN NOT NULL DEFAULT false,
"userId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Notification_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_SharedDocuments" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "_SharedDocuments_AB_unique" ON "_SharedDocuments"("A", "B");
-- CreateIndex
CREATE INDEX "_SharedDocuments_B_index" ON "_SharedDocuments"("B");
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_SharedDocuments" ADD CONSTRAINT "_SharedDocuments_A_fkey" FOREIGN KEY ("A") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_SharedDocuments" ADD CONSTRAINT "_SharedDocuments_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -0,0 +1,49 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Document {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
s3Key String
status String @default("pending") // pending, uploading, completed, failed
sharedWith User[] @relation("SharedDocuments")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
isAdmin Boolean @default(false)
authoredDocuments Document[] @relation("AuthoredDocuments")
sharedDocuments Document[] @relation("SharedDocuments")
notifications Notification[]
}
model Notification {
id Int @id @default(autoincrement())
message String
read Boolean @default(false)
userId Int
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
}

View File

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

View File

@ -0,0 +1,115 @@
import {
Controller,
Get,
Post,
Body,
Param,
Delete,
Put,
UseInterceptors,
UploadedFile,
ParseIntPipe,
UseGuards,
Request,
Req,
} 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';
@Controller('admin')
@UseGuards(JwtAuthGuard, AdminGuard)
export class AdminController {
constructor(
private readonly adminService: AdminService,
private readonly s3Service: S3Service,
) {}
@Post('documents')
@UseInterceptors(FileInterceptor('file'))
createDocument(
@Body() createDocumentDto: CreateDocumentDto,
@UploadedFile() file: Express.Multer.File,
) {
return this.adminService.createDocument(createDocumentDto, file);
}
// @Post('document')
// @UseInterceptors(FileInterceptor('file'))
// async createDocument(
// @Body() createDocumentDto: CreateDocumentDto,
// @UploadedFile() file: Express.Multer.File,
// ) {
// // The actual file upload is handled by the FileInterceptor
// // We just need to pass the file to the service
// return this.adminService.createDocument(createDocumentDto, file);
// }
@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);
}
@Delete('documents/:id')
async deleteDocument(@Param('id') id: string) {
await this.adminService.deleteDocument(+id);
return { message: 'Document deleted successfully' };
}
@Get('users')
getAllUsers() {
return this.adminService.getAllUsers();
}
@Post('users')
async createUser(@Body() createUserDto: CreateUserDto) {
return this.adminService.createUser(createUserDto);
}
@Post('documents/:id/share')
async shareDocument(
@Param('id') id: string,
@Body() { userIds }: { userIds: number[] },
) {
return this.adminService.shareDocument(+id, userIds);
}
@Put('documents/:id/status')
async updateDocumentStatus(
@Param('id') id: string,
@Body() { status }: { status: string },
) {
return this.adminService.updateDocumentStatus(+id, status);
}
async uploadDocument(
@UploadedFile() file: Express.Multer.File,
@Body('title') title: string,
@Request() req,
) {
return this.adminService.uploadDocument(file, title, req.user.userId);
}
@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' };
}
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller';
import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module';
import { S3Module } from '../s3/s3.module';
@Module({
controllers: [AdminController],
providers: [AdminService],
imports: [PrismaModule, S3Module],
})
export class AdminModule {}

View File

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

View File

@ -0,0 +1,202 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { S3Service } from '../s3/s3.service';
import { CreateDocumentDto } from '../dto/create-document.dto';
import { UpdateDocumentDto } from '../dto/update-document.dto';
import { CreateUserDto } from '../dto/create-user.dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AdminService {
constructor(
private prisma: PrismaService,
private s3Service: S3Service,
) {}
// async createDocument(
// createDocumentDto: CreateDocumentDto,
// file: Express.Multer.File,
// ) {
// const { title, content, clientEmail } = createDocumentDto;
// console.log('Received createDocumentDto:', createDocumentDto);
// let author;
// if (clientEmail) {
// author = await this.prisma.user.findUnique({
// where: { email: clientEmail },
// });
// if (!author) {
// throw new NotFoundException(`User with email ${clientEmail} not found`);
// }
// }
// const s3Key = await this.s3Service.uploadFile(file, 'documents');
// return this.prisma.document.create({
// data: {
// title,
// content,
// s3Key,
// ...(author && { author: { connect: { id: author.id } } }),
// },
// });
// }
async createDocument(
createDocumentDto: CreateDocumentDto,
file: Express.Multer.File,
) {
const { title, content, clientEmail } = createDocumentDto;
console.log('Received createDocumentDto:', createDocumentDto);
let authorId: number;
if (clientEmail) {
const author = await this.prisma.user.findUnique({
where: { email: clientEmail },
});
if (!author) {
throw new NotFoundException(`User with email ${clientEmail} not found`);
}
authorId = author.id;
} else {
// If no clientEmail is provided, we'll use a default admin user
// You should replace this with an appropriate default user ID
const defaultAdmin = await this.prisma.user.findFirst({
where: { isAdmin: true },
});
if (!defaultAdmin) {
throw new NotFoundException('No default admin user found');
}
authorId = defaultAdmin.id;
}
const s3Key = await this.s3Service.uploadFile(file, 'documents');
return this.prisma.document.create({
data: {
title,
content,
s3Key,
author: { connect: { id: authorId } },
},
});
}
async getAllDocuments() {
return this.prisma.document.findMany();
}
async updateDocument(
id: number,
updateDocumentDto: UpdateDocumentDto,
file?: Express.Multer.File,
) {
const { ...documentData } = updateDocumentDto;
let s3Key: string | undefined;
if (file) {
const oldDocument = await this.prisma.document.findUnique({
where: { id },
select: { s3Key: true },
});
if (oldDocument?.s3Key) {
await this.s3Service.deleteFile(oldDocument.s3Key);
}
s3Key = await this.s3Service.uploadFile(file, 'documents');
}
return this.prisma.document.update({
where: { id },
data: {
...documentData,
...(s3Key && { s3Key }),
},
});
}
async deleteDocument(id: number) {
const document = await this.prisma.document.findUnique({
where: { id },
select: { s3Key: true },
});
if (document?.s3Key) {
await this.s3Service.deleteFile(document.s3Key);
}
return this.prisma.document.delete({ where: { id } });
}
async getAllUsers() {
return this.prisma.user.findMany();
}
async createUser(createUserDto: CreateUserDto) {
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
return this.prisma.user.create({
data: {
...createUserDto,
password: hashedPassword,
},
});
}
async shareDocument(documentId: number, userIds: number[]) {
const document = await this.prisma.document.update({
where: { id: documentId },
data: {
sharedWith: {
connect: userIds.map((id: number) => ({ id })),
},
},
});
// Create notifications for shared users
await this.prisma.notification.createMany({
data: userIds.map((userId: number) => ({
userId,
message: `A new document "${document.title}" has been shared with you.`,
})),
});
return document;
}
async updateDocumentStatus(documentId: number, status: string) {
return this.prisma.document.update({
where: { id: documentId },
data: { status },
});
}
async uploadDocument(
file: Express.Multer.File,
title: string,
authorId: number,
) {
const key = `documents/${Date.now()}-${file.originalname}`;
const s3Key = await this.s3Service.uploadFile(file, key);
return this.prisma.document.create({
data: {
title,
s3Key,
authorId,
status: 'completed',
},
});
}
async getDocumentUrl(documentId: number) {
const document = await this.prisma.document.findUnique({
where: { id: documentId },
});
if (!document) {
throw new NotFoundException('Document not found');
}
return this.s3Service.getFileUrl(document.s3Key);
}
}

View File

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@ -0,0 +1,44 @@
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';
@Module({
imports: [
// TypeOrmModule.forRoot({
// type: 'postgres',
// host: 'localhost',
// port: 5432,
// username: 'root',
// password: 'admin',
// database: 'imk',
// synchronize: true,
// }),
ConfigModule.forRoot(),
AuthModule,
AdminModule,
ClientModule,
S3Module,
PrismaModule,
],
controllers: [AppController, AuthController],
providers: [
AppService,
UploadService,
DocumentsService,
S3Service,
PrismaService,
],
})
export class AppModule {}

View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -0,0 +1,38 @@
import {
Injectable,
CanActivate,
ExecutionContext,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class AdminGuard implements CanActivate {
private readonly logger = new Logger(AdminGuard.name);
constructor(private prisma: PrismaService) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
async validateRequest(request: any): Promise<boolean> {
const user = await this.prisma.user.findUnique({
where: { id: request.user.userId },
});
return user?.isAdmin === true;
}
// async validateRequest(request: any): Promise<boolean> {
// this.logger.debug(`Validating request for user ID: ${request.user.userId}`);
// const user = await this.prisma.user.findUnique({
// where: { id: request.user.userId },
// });
// const isAdmin = user?.isAdmin === true;
// this.logger.debug(`User is admin: ${isAdmin}`);
// return isAdmin;
// }
}

View File

@ -0,0 +1,40 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { CreateUserDto } from '../dto/create-user.dto';
describe('AuthController', () => {
let controller: AuthController;
let authService: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
{
provide: AuthService,
useValue: {
createUser: jest.fn(),
},
},
],
}).compile();
controller = module.get<AuthController>(AuthController);
authService = module.get<AuthService>(AuthService);
});
describe('register', () => {
it('should call authService.createUser with CreateUserDto', async () => {
const createUserDto: CreateUserDto = {
name: 'testuser',
password: 'password123',
email: 'test@example.com',
};
await controller.register(createUserDto);
expect(authService.createUser).toHaveBeenCalledWith(createUserDto);
});
});
});

View File

@ -0,0 +1,48 @@
import {
Controller,
Post,
Body,
UnauthorizedException,
UseGuards,
Get,
Request,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from '../dto/login.dto';
import { CreateUserDto } from '../dto/create-user.dto';
import { JwtAuthGuard } from './jwt-auth.guard';
import { AdminGuard } from './admin.guard';
//@UseGuards(JwtAuthGuard, AdminGuard)
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
async login(@Body() loginDto: LoginDto) {
const user = await this.authService.validateUser(
loginDto.username,
loginDto.password,
);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
return this.authService.login(user);
}
@Post('register')
async register(@Body() createUserDto: CreateUserDto) {
return this.authService.createUser(createUserDto);
}
//@UseGuards(JwtAuthGuard)
@Post('create-admin')
async createAdmin(@Body() createUserDto: CreateUserDto) {
return this.authService.createUser(createUserDto, true);
}
@UseGuards(JwtAuthGuard)
@Get('user-info')
async getUserInfo(@Request() req) {
return this.authService.getUserInfo(req.user.userId);
}
}

View File

@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
//import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [
PassportModule,
PrismaModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60m' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}

View File

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

View File

@ -0,0 +1,73 @@
import { Injectable, ConflictException } 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';
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async validateUser(username: string, password: string): Promise<any> {
const user = await this.prisma.user.findUnique({
where: { email: username },
});
if (user && (await bcrypt.compare(password, user.password))) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.email, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
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({
data: {
email: createUserDto.email,
password: hashedPassword,
name: createUserDto.name,
isAdmin: isAdmin,
},
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, ...result } = newUser;
console.log(result);
return result;
}
async getUserInfo(userId: number) {
return this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
name: true,
email: true,
isAdmin: true,
},
});
}
}

View File

@ -0,0 +1,3 @@
export const jwtConstants = {
secret: '1234',
};

View File

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View File

@ -0,0 +1,19 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}

View File

@ -0,0 +1,19 @@
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}

View File

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

View File

@ -0,0 +1,15 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ClientService } from './client.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { User } from '../decorators/user.decorator';
@Controller('client')
@UseGuards(JwtAuthGuard)
export class ClientController {
constructor(private readonly clientService: ClientService) {}
@Get('documents')
async getClientDocuments(@User() user) {
return this.clientService.getClientDocuments(user.id);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ClientService } from './client.service';
import { ClientController } from './client.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
providers: [ClientService],
controllers: [ClientController],
imports: [PrismaModule],
})
export class ClientModule {}

View File

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

View File

@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class ClientService {
constructor(private prisma: PrismaService) {}
async getClientDocuments(userId: string) {
return this.prisma.document.findMany({
where: {
authorId: Number(userId),
},
});
}
}

View File

@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);

View File

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

View File

@ -0,0 +1,14 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Document } from '@prisma/client';
@Injectable()
export class DocumentsService {
constructor(private readonly prisma: PrismaService) {}
async findAllForClient(clientId: number): Promise<Document[]> {
return this.prisma.document.findMany({
where: { authorId: clientId },
});
}
}

View File

@ -0,0 +1,11 @@
import { IsString, IsNotEmpty } from 'class-validator';
export class LoginDto {
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
password: string;
}

View File

@ -0,0 +1,18 @@
import { IsString, IsDate, IsUrl } from 'class-validator';
export class ClientDocumentDto {
@IsString()
id: string;
@IsString()
title: string;
@IsString()
content: string;
@IsDate()
createdAt: Date;
@IsUrl()
fileUrl: string;
}

View File

@ -0,0 +1,16 @@
import { IsString, IsOptional, IsEmail } from 'class-validator';
export class CreateDocumentDto {
@IsString()
title: string;
@IsString()
@IsOptional()
content?: string;
@IsEmail()
@IsOptional()
clientEmail?: string;
file: Express.Multer.File;
}

View File

@ -0,0 +1,13 @@
import { IsString, IsEmail, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
}

View File

@ -0,0 +1,11 @@
import { IsString, IsNotEmpty } from 'class-validator';
export class LoginDto {
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
password: string;
}

View File

@ -0,0 +1,18 @@
import { IsString, IsNotEmpty, IsEmail, MinLength } from 'class-validator';
export class RegisterDto {
@IsString()
@IsNotEmpty()
username: string;
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsString()
@IsNotEmpty()
role: 'admin' | 'client';
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateDocumentDto } from './create-document.dto';
export class UpdateDocumentDto extends PartialType(CreateDocumentDto) {}

View File

@ -0,0 +1,9 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3000);
}
bootstrap();

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

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

View File

@ -0,0 +1,16 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { S3Service } from './s3.service';
@Module({
imports: [ConfigModule],
providers: [S3Service],
exports: [S3Service],
})
export class S3Module {}

View File

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

View File

@ -0,0 +1,145 @@
// import { Injectable } from '@nestjs/common';
// import {
// S3Client,
// PutObjectCommand,
// DeleteObjectCommand,
// } from '@aws-sdk/client-s3';
// import { Upload } from '@aws-sdk/lib-storage';
// @Injectable()
// export class S3Service {
// private s3Client: S3Client;
// constructor() {
// this.s3Client = new S3Client({
// region: process.env.AWS_REGION,
// credentials: {
// accessKeyId: process.env.AWS_ACCESS_KEY_ID,
// secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
// },
// });
// }
// async uploadFile(file: Express.Multer.File, key: string): Promise<string> {
// const upload = new Upload({
// client: this.s3Client,
// params: {
// Bucket: process.env.AWS_S3_BUCKET_NAME,
// Key: key,
// Body: file.buffer,
// },
// });
// await upload.done();
// return key;
// }
// async deleteFile(key: string): Promise<void> {
// const command = new DeleteObjectCommand({
// Bucket: process.env.AWS_S3_BUCKET_NAME,
// Key: key,
// });
// await this.s3Client.send(command);
// }
// }
import { Injectable, Logger } from '@nestjs/common';
import {
S3Client,
DeleteObjectCommand,
GetObjectCommand,
ListObjectsCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Upload } from '@aws-sdk/lib-storage';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class S3Service {
private s3Client: S3Client;
private readonly logger = new Logger(S3Service.name);
constructor(private configService: ConfigService) {
this.s3Client = new S3Client({
region: this.configService.get('AWS_REGION'),
endpoint: this.configService.get('AWS_ENDPOINT_URL'),
credentials: {
accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
},
forcePathStyle: true, // Needed for non-AWS S3 compatible services
});
}
// async uploadFile(file: Express.Multer.File, key: string): Promise<string> {
// const upload = new Upload({
// client: this.s3Client,
// params: {
// Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
// Key: key,
// Body: file.buffer,
// ContentType: file.mimetype,
// },
// });
// await upload.done();
// return key;
// }
async uploadFile(file: Express.Multer.File, key: string): Promise<string> {
try {
const uniqueKey = `${Date.now()}-${key}`; // Ensure unique keys
const upload = new Upload({
client: this.s3Client,
params: {
Bucket: process.env.AWS_S3_BUCKET_NAME,
Key: uniqueKey,
Body: file.buffer,
},
});
const result = await upload.done();
console.log(`File uploaded successfully: ${uniqueKey}`);
console.log(result);
return uniqueKey;
} catch (error) {
console.error(`Error uploading file: ${error.message}`);
throw error;
}
}
async deleteFile(key: string): Promise<void> {
const command = new DeleteObjectCommand({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
Key: key,
});
await this.s3Client.send(command);
}
async getFileUrl(key: string): Promise<string> {
const command = new GetObjectCommand({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
Key: key,
});
const url = await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); // URL expires in 1 hour
return url;
}
async testConnection(): Promise<boolean> {
try {
const command = new ListObjectsCommand({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
MaxKeys: 1,
});
const response = await this.s3Client.send(command);
this.logger.log(
`Successfully connected to S3. Bucket contains ${response.Contents?.length || 0} objects.`,
);
return true;
} catch (error) {
this.logger.error('Failed to connect to S3', error);
return false;
}
}
}

View File

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

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { S3Client, S3ClientConfig } from '@aws-sdk/client-s3';
import { ConfigService } from '@nestjs/config';
import { Upload } from '@aws-sdk/lib-storage';
@Injectable()
export class UploadService {
private s3Client: S3Client;
constructor(private configService: ConfigService) {
this.s3Client = new S3Client({
region: this.configService.get('AWS_REGION'),
credentials: {
accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
},
endpoint: this.configService.get('S3_ENDPOINT'),
});
}
async uploadFile(file: Express.Multer.File, key: string): Promise<string> {
const upload = new Upload({
client: this.s3Client,
params: {
Bucket: this.configService.get('S3_BUCKET'),
Key: key,
Body: file.buffer,
},
});
const result = await upload.done();
return result.Location;
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

View File

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

9
frontend/imk/README.md Normal file
View File

@ -0,0 +1,9 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
# imk

16
frontend/imk/index.html Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/logo.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IMK</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

4437
frontend/imk/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
frontend/imk/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "imk",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"axios": "^1.7.7",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.15",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.28",
"tailwindcss": "^3.3.3",
"vite": "^4.4.5"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
frontend/imk/public/1.jpg Normal file

Binary file not shown.

BIN
frontend/imk/public/10.jpg Normal file

Binary file not shown.

BIN
frontend/imk/public/2.jpg Normal file

Binary file not shown.

BIN
frontend/imk/public/3.jpg Normal file

Binary file not shown.

BIN
frontend/imk/public/4.jpg Normal file

Binary file not shown.

BIN
frontend/imk/public/5.jpg Normal file

Binary file not shown.

BIN
frontend/imk/public/6.jpg Normal file

Binary file not shown.

BIN
frontend/imk/public/7.jpg Normal file

Binary file not shown.

BIN
frontend/imk/public/8.jpg Normal file

Binary file not shown.

BIN
frontend/imk/public/9.jpg Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
frontend/imk/public/lab.jpg Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
frontend/imk/public/tuf.png Normal file

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More