init
7
.gitignore
vendored
Normal 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
@ -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
|
||||||
25
backend/imk-backend/.eslintrc.js
Normal 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',
|
||||||
|
},
|
||||||
|
};
|
||||||
4
backend/imk-backend/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
73
backend/imk-backend/README.md
Normal 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>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](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).
|
||||||
8
backend/imk-backend/nest-cli.json
Normal 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
91
backend/imk-backend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Document" ADD COLUMN "s3Key" TEXT;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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"
|
||||||
49
backend/imk-backend/prisma/schema.prisma
Normal 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())
|
||||||
|
}
|
||||||
18
backend/imk-backend/src/admin/admin.controller.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
115
backend/imk-backend/src/admin/admin.controller.ts
Normal 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' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/imk-backend/src/admin/admin.module.ts
Normal 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 {}
|
||||||
18
backend/imk-backend/src/admin/admin.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
202
backend/imk-backend/src/admin/admin.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
backend/imk-backend/src/app.controller.spec.ts
Normal 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!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
backend/imk-backend/src/app.controller.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
44
backend/imk-backend/src/app.module.ts
Normal 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 {}
|
||||||
8
backend/imk-backend/src/app.service.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
||||||
38
backend/imk-backend/src/auth/admin.guard.ts
Normal 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;
|
||||||
|
// }
|
||||||
|
}
|
||||||
40
backend/imk-backend/src/auth/auth.controller.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
48
backend/imk-backend/src/auth/auth.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
backend/imk-backend/src/auth/auth.module.ts
Normal 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 {}
|
||||||
18
backend/imk-backend/src/auth/auth.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
73
backend/imk-backend/src/auth/auth.service.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
3
backend/imk-backend/src/auth/constants.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const jwtConstants = {
|
||||||
|
secret: '1234',
|
||||||
|
};
|
||||||
5
backend/imk-backend/src/auth/jwt-auth.guard.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||||
19
backend/imk-backend/src/auth/jwt.strategy.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
19
backend/imk-backend/src/auth/local.strategy.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
backend/imk-backend/src/client/client.controller.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
backend/imk-backend/src/client/client.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
backend/imk-backend/src/client/client.module.ts
Normal 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 {}
|
||||||
18
backend/imk-backend/src/client/client.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
backend/imk-backend/src/client/client.service.ts
Normal 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),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
8
backend/imk-backend/src/decorators/user.decorator.ts
Normal 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;
|
||||||
|
},
|
||||||
|
);
|
||||||
18
backend/imk-backend/src/documents/documents.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
14
backend/imk-backend/src/documents/documents.service.ts
Normal 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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
11
backend/imk-backend/src/dto/auth.dto.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { IsString, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
18
backend/imk-backend/src/dto/client-document.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
16
backend/imk-backend/src/dto/create-document.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
13
backend/imk-backend/src/dto/create-user.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
11
backend/imk-backend/src/dto/login.dto.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { IsString, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
18
backend/imk-backend/src/dto/register.dto.ts
Normal 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';
|
||||||
|
}
|
||||||
4
backend/imk-backend/src/dto/update-document.dto.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
|
import { CreateDocumentDto } from './create-document.dto';
|
||||||
|
|
||||||
|
export class UpdateDocumentDto extends PartialType(CreateDocumentDto) {}
|
||||||
9
backend/imk-backend/src/main.ts
Normal 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();
|
||||||
8
backend/imk-backend/src/prisma/prisma.module.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
18
backend/imk-backend/src/prisma/prisma.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
16
backend/imk-backend/src/prisma/prisma.service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
backend/imk-backend/src/s3/s3.module.ts
Normal 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 {}
|
||||||
18
backend/imk-backend/src/s3/s3.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
145
backend/imk-backend/src/s3/s3.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
backend/imk-backend/src/upload/upload.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
33
backend/imk-backend/src/upload/upload.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
backend/imk-backend/tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
21
backend/imk-backend/tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
20
frontend/imk/.eslintrc.cjs
Normal 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
@ -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
@ -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
34
frontend/imk/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/imk/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
frontend/imk/public/1.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
frontend/imk/public/10.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
frontend/imk/public/2.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
frontend/imk/public/3.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/imk/public/4.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
frontend/imk/public/5.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
frontend/imk/public/6.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
frontend/imk/public/7.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
frontend/imk/public/8.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
frontend/imk/public/9.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
frontend/imk/public/armatura.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
frontend/imk/public/background.jpg
Normal file
|
After Width: | Height: | Size: 1003 KiB |
BIN
frontend/imk/public/consalting.webp
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
frontend/imk/public/consalting1.jpg
Normal file
|
After Width: | Height: | Size: 774 KiB |
BIN
frontend/imk/public/consulting2.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/imk/public/ekstrakcija.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
frontend/imk/public/geotehnika.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
frontend/imk/public/imklogorgb.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/imk/public/kinenje.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
frontend/imk/public/kocka.webp
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
frontend/imk/public/kocki.webp
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
frontend/imk/public/lab.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
frontend/imk/public/lab.webp
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
frontend/imk/public/labFinal.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/imk/public/labfinal.webp
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
frontend/imk/public/logo.jpg
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
frontend/imk/public/naum1.webp
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
frontend/imk/public/naum2.webp
Normal file
|
After Width: | Height: | Size: 310 KiB |
BIN
frontend/imk/public/profili.jpg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
frontend/imk/public/sertifikat1.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
frontend/imk/public/sertifikat2.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
frontend/imk/public/sertifikat3.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
frontend/imk/public/sertifikat4.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
frontend/imk/public/tuf.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
frontend/imk/public/wallscener.jpeg
Normal file
|
After Width: | Height: | Size: 4.5 KiB |