Compare commits

...

3 Commits

Author SHA1 Message Date
7c02511dab updated
updated
2025-02-24 23:47:32 +01:00
3e298a1eef final version
needs file status update[pending, finished]
2025-02-24 23:43:50 +01:00
c7d4b95631 final version
complete redesign
2025-02-24 23:40:35 +01:00
169 changed files with 92482 additions and 0 deletions

34
.cursorrules Normal file
View File

@ -0,0 +1,34 @@
You are a Senior full-stack Developer and an Expert in Nestjs, Prisma,ReactJS, JavaScript, TypeScript, HTML, CSS and modern UI/UX frameworks (e.g., TailwindCSS, Shadcn, Radix). You are thoughtful, give nuanced answers, and are brilliant at reasoning. You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning.
- Follow the users requirements carefully & to the letter.
- First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail.
- Confirm, then write code!
- Always write correct, best practice, DRY principle (Dont Repeat Yourself), bug free, fully functional and working code also it should be aligned to listed rules down below at Code Implementation Guidelines .
- Focus on easy and readability code, over being performant.
- Fully implement all requested functionality.
- Leave NO todos, placeholders or missing pieces.
- Ensure code is complete! Verify thoroughly finalised.
- Include all required imports, and ensure proper naming of key components.
- Be concise Minimize any other prose.
- If you think there might not be a correct answer, you say so.
- If you do not know the answer, say so, instead of guessing.
### Coding Environment
The user asks questions about the following coding languages:
- ReactJS
- Nestjs
- Prisma
- JavaScript
- TypeScript
- TailwindCSS
- HTML
- CSS
### Code Implementation Guidelines
Follow these rules when you write code:
- Use early returns whenever possible to make the code more readable.
- Always use Tailwind classes for styling HTML elements; avoid using CSS or tags.
- Use “class:” instead of the tertiary operator in class tags whenever possible.
- Use descriptive variable and function/const names. Also, event functions should be named with a “handle” prefix, like “handleClick” for onClick and “handleKeyDown” for onKeyDown.
- Implement accessibility features on elements. For example, a tag should have a tabindex=“0”, aria-label, on:click, and on:keydown, and similar attributes.
- Use consts instead of functions, for example, “const toggle = () =>”. Also, define a type if possible.

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
application/cache
backend/node_modules
backend/dist
backend/test
frontend/node_modules
frontend/dist
frontend/.vite
node_modules

14
backend/.env Normal file
View File

@ -0,0 +1,14 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://root:irina76@localhost:5432/imk?schema=public"
JWT_SECRET=some-secret
AWS_REGION=EU2
AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6
AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4
AWS_S3_BUCKET_NAME=imk-data
AWS_ENDPOINT_URL=https://eu2.contabostorage.com

73
backend/README.md Normal file
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).

8
backend/nest-cli.json Normal file
View File

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

11352
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

94
backend/package.json Normal file
View File

@ -0,0 +1,94 @@
{
"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.12.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.12.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"
},
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
"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,63 @@
-- CreateTable
CREATE TABLE "Document" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"authorId" INTEGER NOT NULL,
"s3Key" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
);
-- 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 "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 "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "_SharedDocuments_AB_unique" ON "_SharedDocuments"("A", "B");
-- CreateIndex
CREATE INDEX "_SharedDocuments_B_index" ON "_SharedDocuments"("B");
-- AddForeignKey
ALTER TABLE "Document" ADD CONSTRAINT "Document_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- 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,28 @@
/*
Warnings:
- You are about to drop the column `authorId` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `published` on the `Document` table. All the data in the column will be lost.
- You are about to drop the `_SharedDocuments` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `sharedWithId` to the `Document` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "Document" DROP CONSTRAINT "Document_authorId_fkey";
-- DropForeignKey
ALTER TABLE "_SharedDocuments" DROP CONSTRAINT "_SharedDocuments_A_fkey";
-- DropForeignKey
ALTER TABLE "_SharedDocuments" DROP CONSTRAINT "_SharedDocuments_B_fkey";
-- AlterTable
ALTER TABLE "Document" DROP COLUMN "authorId",
DROP COLUMN "published",
ADD COLUMN "sharedWithId" INTEGER NOT NULL;
-- DropTable
DROP TABLE "_SharedDocuments";
-- AddForeignKey
ALTER TABLE "Document" ADD CONSTRAINT "Document_sharedWithId_fkey" FOREIGN KEY ("sharedWithId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,44 @@
/*
Warnings:
- You are about to drop the column `content` on the `Document` table. All the data in the column will be lost.
- You are about to drop the column `sharedWithId` on the `Document` table. All the data in the column will be lost.
- Added the required column `uploadedById` to the `Document` table without a default value. This is not possible if the table is not empty.
- Added the required column `documentId` to the `Notification` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "Document" DROP CONSTRAINT "Document_sharedWithId_fkey";
-- AlterTable
ALTER TABLE "Document" DROP COLUMN "content",
DROP COLUMN "sharedWithId",
ADD COLUMN "uploadedById" INTEGER NOT NULL,
ALTER COLUMN "status" DROP DEFAULT;
-- AlterTable
ALTER TABLE "Notification" ADD COLUMN "documentId" INTEGER NOT NULL;
-- 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 "Document" ADD CONSTRAINT "Document_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("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,43 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Document {
id Int @id @default(autoincrement())
title String
s3Key String
status String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
uploadedById Int
uploadedBy User @relation("UploadedDocuments", fields: [uploadedById], references: [id])
Notification Notification[]
sharedWith User[] @relation("SharedDocuments")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
isAdmin Boolean @default(false)
uploadedDocuments Document[] @relation("UploadedDocuments")
Notification Notification[]
sharedDocuments Document[] @relation("SharedDocuments")
}
model Notification {
id Int @id @default(autoincrement())
message String
read Boolean @default(false)
userId Int
createdAt DateTime @default(now())
documentId Int
document Document @relation(fields: [documentId], references: [id])
user User @relation(fields: [userId], references: [id])
}

82
backend/prisma/seed.js Normal file
View File

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

30
backend/prisma/seed.ts Normal file
View File

@ -0,0 +1,30 @@
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
const hashedPassword = await bcrypt.hash('admin123', 10);
const admin = await prisma.user.upsert({
where: { email: 'admin@example.com' },
update: {},
create: {
email: 'admin@example.com',
name: 'Admin User',
password: hashedPassword,
isAdmin: true,
},
});
console.log({ admin });
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

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,134 @@
import {
Controller,
Get,
Post,
Body,
Param,
Put,
UseInterceptors,
UploadedFile,
ParseIntPipe,
UseGuards,
BadRequestException,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { AdminService } from './admin.service';
import { UpdateDocumentDto } from '../dto/update-document.dto';
import { AdminGuard } from '../auth/admin.guard';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CreateUserDto } from '../dto/create-user.dto';
import { S3Service } from 'src/s3/s3.service';
import { PrismaService } from 'src/prisma/prisma.service';
@Controller('admin')
@UseGuards(JwtAuthGuard, AdminGuard)
export class AdminController {
constructor(
private readonly adminService: AdminService,
private readonly s3Service: S3Service,
private readonly prisma: PrismaService,
) {}
@Get('documents')
getAllDocuments() {
return this.adminService.getAllDocuments();
}
@Put('documents/:id')
@UseInterceptors(FileInterceptor('file'))
updateDocument(
@Param('id', ParseIntPipe) id: number,
@Body() updateDocumentDto: UpdateDocumentDto,
@UploadedFile() file?: Express.Multer.File,
) {
return this.adminService.updateDocument(id, updateDocumentDto, file);
}
@Post('documents')
@UseInterceptors(FileInterceptor('file'))
async uploadDocument(
@UploadedFile() file: Express.Multer.File,
@Body('title') title: string,
@Body('sharedWithId') sharedWithId: string, // Accept as string first
@Body('uploadedById') uploadedById: string // Accept as string first
) {
if (!sharedWithId || !uploadedById) {
throw new BadRequestException('sharedWithId and uploadedById are required');
}
// Parse the string values to numbers
const parsedSharedWithId = parseInt(sharedWithId, 10);
const parsedUploadedById = parseInt(uploadedById, 10);
// Validate that the parsing was successful
if (isNaN(parsedSharedWithId) || isNaN(parsedUploadedById)) {
throw new BadRequestException('sharedWithId and uploadedById must be valid numbers');
}
const document = await this.adminService.uploadDocument(
file,
title,
parsedSharedWithId,
parsedUploadedById
);
return document;
}
@Get('users')
getAllUsers() {
return this.adminService.getAllUsers();
}
// @Post('test-document')
// async testDocumentCreation() {
// try {
// const document = await this.prisma.document.create({
// data: {
// title: 'Test Document',
// s3Key: 'test-key',
// status: 'completed',
// sharedWith: {
// connect: { id: 2 } // ID of 'pero' user
// }
// },
// });
// return document;
// } catch (error) {
// console.error('Test document creation error:', error);
// throw error;
// }
// }
@Post('users')
async createUser(@Body() createUserDto: CreateUserDto) {
return this.adminService.createUser(createUserDto);
}
@Post('documents/:id/share')
async shareDocument(
@Param('id') id: string,
@Body() { userId }: { userId: number },
) {
return this.adminService.shareDocument(+id, userId);
}
@Put('documents/:id/status')
async updateDocumentStatus(
@Param('id') id: string,
@Body() { status }: { status: string },
) {
return this.adminService.updateDocumentStatus(+id, status);
}
// @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,129 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { S3Service } from '../s3/s3.service';
import { UpdateDocumentDto } from '../dto/update-document.dto';
import { CreateUserDto } from '../dto/create-user.dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AdminService {
constructor(
private readonly prisma: PrismaService,
private readonly s3Service: S3Service,
) {}
async getAllDocuments() {
return this.prisma.document.findMany({
include: {
uploadedBy: {
select: {
id: true,
name: true,
email: true,
},
},
sharedWith: {
select: {
id: true,
name: true,
email: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
});
}
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 updateDocument(
id: number,
updateDocumentDto: UpdateDocumentDto,
file?: Express.Multer.File,
) {
let s3Key = undefined;
if (file) {
s3Key = await this.s3Service.uploadFile(file, 'documents');
}
return this.prisma.document.update({
where: { id },
data: {
...updateDocumentDto,
...(s3Key && { s3Key }),
},
});
}
async shareDocument(documentId: number, userId: number) {
return this.prisma.document.update({
where: { id: documentId },
data: {
sharedWith: {
connect: { id: userId },
},
},
});
}
async updateDocumentStatus(documentId: number, status: string) {
return this.prisma.document.update({
where: { id: documentId },
data: { status },
});
}
async uploadDocument(
file: Express.Multer.File,
title: string,
sharedWithId: number,
uploadedById: number
) {
const s3Key = await this.s3Service.uploadFile(file, 'documents');
return this.prisma.document.create({
data: {
title,
s3Key,
status: 'pending',
sharedWith: {
connect: { id: sharedWithId }
},
uploadedBy: {
connect: { id: uploadedById }
}
},
include: {
uploadedBy: {
select: {
id: true,
name: true,
email: true,
},
},
sharedWith: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
}
}

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();
}
}

52
backend/src/app.module.ts Normal file
View File

@ -0,0 +1,52 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
//import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminModule } from './admin/admin.module';
import { ClientModule } from './client/client.module';
import { UploadService } from './upload/upload.service';
import { DocumentsService } from './documents/documents.service';
import { S3Service } from './s3/s3.service';
import { S3Module } from './s3/s3.module';
import { PrismaService } from './prisma/prisma.service';
import { PrismaModule } from './prisma/prisma.module';
import { ConfigModule } from '@nestjs/config';
import { AuthController } from './auth/auth.controller';
import { DocumentsController } from './documents/documents.controller';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
// TypeOrmModule.forRoot({
// type: 'postgres',
// host: 'localhost',
// port: 5432,
// username: 'root',
// password: 'admin',
// database: 'imk',
// synchronize: true,
// }),
ConfigModule.forRoot({
isGlobal: true,
}),
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' },
}),
AuthModule,
AdminModule,
ClientModule,
S3Module,
PrismaModule,
],
controllers: [AppController, AuthController, DocumentsController],
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,99 @@
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';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
private configService: ConfigService,
) {}
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),
// };
const payload = { username: user.username, sub: user.id };
console.log(payload);
return {
access_token: this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET'),
}),
};
}
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,
// },
// });
// }
async getUserInfo(userId: number) {
if (!userId) {
throw new Error('User ID is required');
}
return this.prisma.user.findUnique({
where: {
id: userId, // Make sure userId is properly passed and converted to number if needed
},
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 { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_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.getDocuments(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,35 @@
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),
// },
// });
// }
async getDocuments(userId: string) {
return this.prisma.document.findMany({
where: {
sharedWith: {
some: {
id: Number(userId),
},
},
},
include: {
sharedWith: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
}
}

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 { DocumentsController } from './documents.controller';
describe('DocumentsController', () => {
let controller: DocumentsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DocumentsController],
}).compile();
controller = module.get<DocumentsController>(DocumentsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,81 @@
import { Controller, Get, Param, Req, Res, UseGuards, Logger, Request } from '@nestjs/common';
import { Response } from 'express';
import { DocumentsService } from './documents.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { S3Service } from '../s3/s3.service';
interface S3File {
buffer: Buffer;
contentType: string;
contentLength: number;
fileName: string;
}
@Controller('documents')
export class DocumentsController {
private readonly logger = new Logger(DocumentsController.name);
constructor(
private readonly documentsService: DocumentsService,
private readonly s3Service: S3Service
) {}
@Get('shared/:userId')
@UseGuards(JwtAuthGuard)
async getSharedDocuments(@Param('userId') userId: string) {
return this.documentsService.getClientDocuments(Number(userId));
}
@Get('download/:key')
@UseGuards(JwtAuthGuard)
async downloadDocument(
@Param('key') key: string,
@Request() req,
@Res() res: Response
) {
try {
this.logger.debug(`Download request for key: ${key}`);
const decodedKey = decodeURIComponent(key);
this.logger.debug(`Decoded key: ${decodedKey}`);
// Get document from database first to verify access
const document = await this.documentsService.findDocumentByS3Key(decodedKey);
if (!document) {
return res.status(404).json({ message: 'Document not found' });
}
// Verify user has access to this document
const hasAccess = await this.documentsService.verifyDocumentAccess(
document.id,
req.user.id
);
if (!hasAccess) {
return res.status(403).json({ message: 'Access denied' });
}
// Get the file from S3
const file = await this.s3Service.getFile(decodedKey);
if (!file || !file.buffer) {
return res.status(404).json({ message: 'File not found in storage' });
}
res.set({
'Content-Type': file.contentType || 'application/octet-stream',
'Content-Length': file.contentLength,
'Content-Disposition': `attachment; filename="${encodeURIComponent(file.fileName)}"`,
});
return res.send(file.buffer);
} catch (error) {
this.logger.error('Download error:', error);
return res.status(500).json({
message: 'Failed to download file',
error: error.message
});
}
}
}

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,120 @@
import { Injectable, Logger, NotFoundException, ForbiddenException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { S3Service } from '../s3/s3.service';
import { Document, User, Prisma } from '@prisma/client';
@Injectable()
export class DocumentsService {
private readonly logger = new Logger(DocumentsService.name);
constructor(
private readonly prisma: PrismaService,
private readonly s3Service: S3Service
) {}
async findDocumentByS3Key(s3Key: string) {
return this.prisma.document.findFirst({
where: { s3Key },
include: {
uploadedBy: true,
sharedWith: true,
},
});
}
async verifyDocumentAccess(documentId: number, userId: number): Promise<boolean> {
const document = await this.prisma.document.findUnique({
where: { id: documentId },
include: {
uploadedBy: true,
sharedWith: {
where: {
id: userId
}
}
},
});
if (!document) {
return false;
}
// User has access if they uploaded the document or it's shared with them
return document.uploadedBy.id === userId || document.sharedWith.length > 0;
}
async getClientDocuments(clientId: number) {
return this.prisma.document.findMany({
where: {
OR: [
{ uploadedById: clientId },
{
sharedWith: {
some: {
id: clientId
}
}
}
]
},
include: {
uploadedBy: {
select: {
id: true,
name: true,
email: true
}
},
sharedWith: {
select: {
id: true,
name: true,
email: true
}
}
},
orderBy: {
createdAt: 'desc'
}
});
}
async uploadDocument(
file: Express.Multer.File,
title: string,
sharedWithId: number,
uploadedById: number
) {
const s3Key = await this.s3Service.uploadFile(file, 'documents');
return this.prisma.document.create({
data: {
title,
s3Key,
status: 'pending',
uploadedBy: {
connect: { id: uploadedById }
},
sharedWith: {
connect: { id: sharedWithId }
}
},
include: {
uploadedBy: {
select: {
id: true,
name: true,
email: true,
},
},
sharedWith: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
}
}

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,16 @@
import { IsString, IsEmail, MinLength, IsBoolean } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
@IsBoolean()
isAdmin: boolean = false;
}

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,6 @@
export interface FileResponse {
buffer: Buffer;
contentType: string;
contentLength: number;
fileName: string;
}

View File

@ -0,0 +1,6 @@
export interface S3File {
buffer: Buffer;
contentType: string;
contentLength: number;
fileName: string;
}

9
backend/src/main.ts Normal file
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,72 @@
import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { S3File } from '../interfaces/s3-file.interface';
@Injectable()
export class S3Service {
private readonly s3Client: S3Client;
private readonly logger = new Logger(S3Service.name);
constructor(private readonly configService: ConfigService) {
this.s3Client = new S3Client({
region: this.configService.get<string>('AWS_REGION'),
credentials: {
accessKeyId: this.configService.get<string>('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.configService.get<string>('AWS_SECRET_ACCESS_KEY'),
},
endpoint: this.configService.get<string>('AWS_ENDPOINT_URL'),
forcePathStyle: true, // Required for Contabo Object Storage
});
}
async uploadFile(file: Express.Multer.File, folder: string): Promise<string> {
try {
const key = `${folder}/${Date.now()}-${file.originalname}`;
const command = new PutObjectCommand({
Bucket: this.configService.get<string>('AWS_S3_BUCKET_NAME'),
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
});
await this.s3Client.send(command);
this.logger.debug(`File uploaded successfully: ${key}`);
return key;
} catch (error) {
this.logger.error(`Error uploading file to S3: ${error.message}`);
throw new InternalServerErrorException('Failed to upload file to storage');
}
}
async getFile(key: string): Promise<S3File> {
try {
const command = new GetObjectCommand({
Bucket: this.configService.get<string>('AWS_S3_BUCKET_NAME'),
Key: key,
});
const response = await this.s3Client.send(command);
const chunks = [];
for await (const chunk of response.Body as any) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
return {
buffer,
contentType: response.ContentType || 'application/octet-stream',
contentLength: response.ContentLength || buffer.length,
fileName: key.split('/').pop() || 'download',
};
} catch (error) {
this.logger.error(`Error getting file from S3: ${error.message}`);
throw new InternalServerErrorException('Failed to retrieve file from storage');
}
}
}

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"]
}

21
backend/tsconfig.json Normal file
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
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,77 @@
{
"hash": "6aa7385c",
"browserHash": "a3876199",
"optimized": {
"react": {
"src": "../../node_modules/react/index.js",
"file": "react.js",
"fileHash": "eec1dd81",
"needsInterop": true
},
"react-dom/client": {
"src": "../../node_modules/react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "be85cf8b",
"needsInterop": true
},
"react-router-dom": {
"src": "../../node_modules/react-router-dom/dist/index.js",
"file": "react-router-dom.js",
"fileHash": "dc3bbb9d",
"needsInterop": false
},
"@headlessui/react": {
"src": "../../node_modules/@headlessui/react/dist/headlessui.esm.js",
"file": "@headlessui_react.js",
"fileHash": "a4ed943c",
"needsInterop": false
},
"@heroicons/react/24/outline": {
"src": "../../node_modules/@heroicons/react/24/outline/esm/index.js",
"file": "@heroicons_react_24_outline.js",
"fileHash": "08a02318",
"needsInterop": false
},
"@heroicons/react/20/solid": {
"src": "../../node_modules/@heroicons/react/20/solid/esm/index.js",
"file": "@heroicons_react_20_solid.js",
"fileHash": "a26ad170",
"needsInterop": false
},
"framer-motion": {
"src": "../../node_modules/framer-motion/dist/es/index.mjs",
"file": "framer-motion.js",
"fileHash": "d4deaa03",
"needsInterop": false
},
"react-icons/fi": {
"src": "../../node_modules/react-icons/fi/index.mjs",
"file": "react-icons_fi.js",
"fileHash": "8894f9b1",
"needsInterop": false
},
"date-fns": {
"src": "../../node_modules/date-fns/index.js",
"file": "date-fns.js",
"fileHash": "51d96688",
"needsInterop": false
},
"axios": {
"src": "../../node_modules/axios/index.js",
"file": "axios.js",
"fileHash": "38b66fb3",
"needsInterop": false
}
},
"chunks": {
"chunk-K2BGRD5Z": {
"file": "chunk-K2BGRD5Z.js"
},
"chunk-G4O6EYSD": {
"file": "chunk-G4O6EYSD.js"
},
"chunk-ZC22LKFR": {
"file": "chunk-ZC22LKFR.js"
}
}
}

2548
frontend/.vite/deps/axios.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,42 @@
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
export {
__commonJS,
__export,
__toESM,
__publicField
};
//# sourceMappingURL=chunk-ZC22LKFR.js.map

View File

@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
{
"type": "module"
}

39
frontend/.vite/deps/react-dom_client.js vendored Normal file
View File

@ -0,0 +1,39 @@
import {
require_react_dom
} from "./chunk-K2BGRD5Z.js";
import "./chunk-G4O6EYSD.js";
import {
__commonJS
} from "./chunk-ZC22LKFR.js";
// node_modules/react-dom/client.js
var require_client = __commonJS({
"node_modules/react-dom/client.js"(exports) {
var m = require_react_dom();
if (false) {
exports.createRoot = m.createRoot;
exports.hydrateRoot = m.hydrateRoot;
} else {
i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
exports.createRoot = function(c, o) {
i.usingClientEntryPoint = true;
try {
return m.createRoot(c, o);
} finally {
i.usingClientEntryPoint = false;
}
};
exports.hydrateRoot = function(c, h, o) {
i.usingClientEntryPoint = true;
try {
return m.hydrateRoot(c, h, o);
} finally {
i.usingClientEntryPoint = false;
}
};
}
var i;
}
});
export default require_client();
//# sourceMappingURL=react-dom_client.js.map

View File

@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../node_modules/react-dom/client.js"],
"sourcesContent": ["'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n"],
"mappings": ";;;;;;;;;AAAA;AAAA;AAEA,QAAI,IAAI;AACR,QAAI,OAAuC;AACzC,cAAQ,aAAa,EAAE;AACvB,cAAQ,cAAc,EAAE;AAAA,IAC1B,OAAO;AACD,UAAI,EAAE;AACV,cAAQ,aAAa,SAAS,GAAG,GAAG;AAClC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,WAAW,GAAG,CAAC;AAAA,QAC1B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AACA,cAAQ,cAAc,SAAS,GAAG,GAAG,GAAG;AACtC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,YAAY,GAAG,GAAG,CAAC;AAAA,QAC9B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAjBM;AAAA;AAAA;",
"names": []
}

1306
frontend/.vite/deps/react-icons_fi.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

4947
frontend/.vite/deps/react-router-dom.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6
frontend/.vite/deps/react.js vendored Normal file
View File

@ -0,0 +1,6 @@
import {
require_react
} from "./chunk-G4O6EYSD.js";
import "./chunk-ZC22LKFR.js";
export default require_react();
//# sourceMappingURL=react.js.map

View File

@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

9
frontend/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/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>

4536
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
frontend/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"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.19",
"@heroicons/react": "^2.2.0",
"axios": "^1.7.7",
"date-fns": "^4.1.0",
"framer-motion": "^11.18.2",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.5.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.20",
"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.47",
"tailwindcss": "^3.4.14",
"vite": "^4.4.5"
}
}

View File

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

BIN
frontend/public/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
frontend/public/10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
frontend/public/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
frontend/public/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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