Compare commits
3 Commits
6553b1c880
...
7c02511dab
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c02511dab | |||
| 3e298a1eef | |||
| c7d4b95631 |
34
.cursorrules
Normal file
34
.cursorrules
Normal 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 user’s 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 todo’s, 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
10
.gitignore
vendored
Normal 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
14
backend/.env
Normal 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
73
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/nest-cli.json
Normal file
8
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
|
||||||
|
}
|
||||||
|
}
|
||||||
11352
backend/package-lock.json
generated
Normal file
11352
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
94
backend/package.json
Normal file
94
backend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
63
backend/prisma/migrations/20241029013255_init/migration.sql
Normal file
63
backend/prisma/migrations/20241029013255_init/migration.sql
Normal 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;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
3
backend/prisma/migrations/migration_lock.toml
Normal file
3
backend/prisma/migrations/migration_lock.toml
Normal 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"
|
||||||
43
backend/prisma/schema.prisma
Normal file
43
backend/prisma/schema.prisma
Normal 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
82
backend/prisma/seed.js
Normal 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
30
backend/prisma/seed.ts
Normal 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();
|
||||||
|
});
|
||||||
18
backend/src/admin/admin.controller.spec.ts
Normal file
18
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
134
backend/src/admin/admin.controller.ts
Normal file
134
backend/src/admin/admin.controller.ts
Normal 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' };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
12
backend/src/admin/admin.module.ts
Normal file
12
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/src/admin/admin.service.spec.ts
Normal file
18
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
129
backend/src/admin/admin.service.ts
Normal file
129
backend/src/admin/admin.service.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
22
backend/src/app.controller.spec.ts
Normal file
22
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/src/app.controller.ts
Normal file
12
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
52
backend/src/app.module.ts
Normal file
52
backend/src/app.module.ts
Normal 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 {}
|
||||||
8
backend/src/app.service.ts
Normal file
8
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/src/auth/admin.guard.ts
Normal file
38
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/src/auth/auth.controller.spec.ts
Normal file
40
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/src/auth/auth.controller.ts
Normal file
48
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/src/auth/auth.module.ts
Normal file
23
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/src/auth/auth.service.spec.ts
Normal file
18
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
99
backend/src/auth/auth.service.ts
Normal file
99
backend/src/auth/auth.service.ts
Normal 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
3
backend/src/auth/constants.ts
Normal file
3
backend/src/auth/constants.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const jwtConstants = {
|
||||||
|
secret: '1234',
|
||||||
|
};
|
||||||
5
backend/src/auth/jwt-auth.guard.ts
Normal file
5
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/src/auth/jwt.strategy.ts
Normal file
19
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 { 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
19
backend/src/auth/local.strategy.ts
Normal file
19
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/src/client/client.controller.spec.ts
Normal file
18
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/src/client/client.controller.ts
Normal file
15
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.getDocuments(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
backend/src/client/client.module.ts
Normal file
11
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/src/client/client.service.spec.ts
Normal file
18
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
35
backend/src/client/client.service.ts
Normal file
35
backend/src/client/client.service.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
8
backend/src/decorators/user.decorator.ts
Normal file
8
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/src/documents/documents.controller.spec.ts
Normal file
18
backend/src/documents/documents.controller.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
81
backend/src/documents/documents.controller.ts
Normal file
81
backend/src/documents/documents.controller.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
backend/src/documents/documents.service.spec.ts
Normal file
18
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
120
backend/src/documents/documents.service.ts
Normal file
120
backend/src/documents/documents.service.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
11
backend/src/dto/auth.dto.ts
Normal file
11
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/src/dto/client-document.dto.ts
Normal file
18
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/src/dto/create-document.dto.ts
Normal file
16
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;
|
||||||
|
}
|
||||||
16
backend/src/dto/create-user.dto.ts
Normal file
16
backend/src/dto/create-user.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
11
backend/src/dto/login.dto.ts
Normal file
11
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/src/dto/register.dto.ts
Normal file
18
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/src/dto/update-document.dto.ts
Normal file
4
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) {}
|
||||||
6
backend/src/interfaces/file-response.interface.ts
Normal file
6
backend/src/interfaces/file-response.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface FileResponse {
|
||||||
|
buffer: Buffer;
|
||||||
|
contentType: string;
|
||||||
|
contentLength: number;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
6
backend/src/interfaces/s3-file.interface.ts
Normal file
6
backend/src/interfaces/s3-file.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface S3File {
|
||||||
|
buffer: Buffer;
|
||||||
|
contentType: string;
|
||||||
|
contentLength: number;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
9
backend/src/main.ts
Normal file
9
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/src/prisma/prisma.module.ts
Normal file
8
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/src/prisma/prisma.service.spec.ts
Normal file
18
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/src/prisma/prisma.service.ts
Normal file
16
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/src/s3/s3.module.ts
Normal file
10
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/src/s3/s3.service.spec.ts
Normal file
18
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
72
backend/src/s3/s3.service.ts
Normal file
72
backend/src/s3/s3.service.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
18
backend/src/upload/upload.service.spec.ts
Normal file
18
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/src/upload/upload.service.ts
Normal file
33
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/tsconfig.build.json
Normal file
4
backend/tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
21
backend/tsconfig.json
Normal file
21
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
|
||||||
|
}
|
||||||
|
}
|
||||||
4046
frontend/.vite/deps/@headlessui_react.js
Normal file
4046
frontend/.vite/deps/@headlessui_react.js
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/.vite/deps/@headlessui_react.js.map
Normal file
7
frontend/.vite/deps/@headlessui_react.js.map
Normal file
File diff suppressed because one or more lines are too long
8725
frontend/.vite/deps/@heroicons_react_20_solid.js
Normal file
8725
frontend/.vite/deps/@heroicons_react_20_solid.js
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/.vite/deps/@heroicons_react_20_solid.js.map
Normal file
7
frontend/.vite/deps/@heroicons_react_20_solid.js.map
Normal file
File diff suppressed because one or more lines are too long
9456
frontend/.vite/deps/@heroicons_react_24_outline.js
Normal file
9456
frontend/.vite/deps/@heroicons_react_24_outline.js
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/.vite/deps/@heroicons_react_24_outline.js.map
Normal file
7
frontend/.vite/deps/@heroicons_react_24_outline.js.map
Normal file
File diff suppressed because one or more lines are too long
77
frontend/.vite/deps/_metadata.json
Normal file
77
frontend/.vite/deps/_metadata.json
Normal 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
2548
frontend/.vite/deps/axios.js
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/.vite/deps/axios.js.map
Normal file
7
frontend/.vite/deps/axios.js.map
Normal file
File diff suppressed because one or more lines are too long
1905
frontend/.vite/deps/chunk-G4O6EYSD.js
Normal file
1905
frontend/.vite/deps/chunk-G4O6EYSD.js
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/.vite/deps/chunk-G4O6EYSD.js.map
Normal file
7
frontend/.vite/deps/chunk-G4O6EYSD.js.map
Normal file
File diff suppressed because one or more lines are too long
21614
frontend/.vite/deps/chunk-K2BGRD5Z.js
Normal file
21614
frontend/.vite/deps/chunk-K2BGRD5Z.js
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/.vite/deps/chunk-K2BGRD5Z.js.map
Normal file
7
frontend/.vite/deps/chunk-K2BGRD5Z.js.map
Normal file
File diff suppressed because one or more lines are too long
42
frontend/.vite/deps/chunk-ZC22LKFR.js
Normal file
42
frontend/.vite/deps/chunk-ZC22LKFR.js
Normal 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
|
||||||
7
frontend/.vite/deps/chunk-ZC22LKFR.js.map
Normal file
7
frontend/.vite/deps/chunk-ZC22LKFR.js.map
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
6138
frontend/.vite/deps/date-fns.js
Normal file
6138
frontend/.vite/deps/date-fns.js
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/.vite/deps/date-fns.js.map
Normal file
7
frontend/.vite/deps/date-fns.js.map
Normal file
File diff suppressed because one or more lines are too long
11164
frontend/.vite/deps/framer-motion.js
Normal file
11164
frontend/.vite/deps/framer-motion.js
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/.vite/deps/framer-motion.js.map
Normal file
7
frontend/.vite/deps/framer-motion.js.map
Normal file
File diff suppressed because one or more lines are too long
3
frontend/.vite/deps/package.json
Normal file
3
frontend/.vite/deps/package.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
39
frontend/.vite/deps/react-dom_client.js
vendored
Normal file
39
frontend/.vite/deps/react-dom_client.js
vendored
Normal 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
|
||||||
7
frontend/.vite/deps/react-dom_client.js.map
Normal file
7
frontend/.vite/deps/react-dom_client.js.map
Normal 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
1306
frontend/.vite/deps/react-icons_fi.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/.vite/deps/react-icons_fi.js.map
Normal file
7
frontend/.vite/deps/react-icons_fi.js.map
Normal file
File diff suppressed because one or more lines are too long
4947
frontend/.vite/deps/react-router-dom.js
vendored
Normal file
4947
frontend/.vite/deps/react-router-dom.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/.vite/deps/react-router-dom.js.map
Normal file
7
frontend/.vite/deps/react-router-dom.js.map
Normal file
File diff suppressed because one or more lines are too long
6
frontend/.vite/deps/react.js
vendored
Normal file
6
frontend/.vite/deps/react.js
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {
|
||||||
|
require_react
|
||||||
|
} from "./chunk-G4O6EYSD.js";
|
||||||
|
import "./chunk-ZC22LKFR.js";
|
||||||
|
export default require_react();
|
||||||
|
//# sourceMappingURL=react.js.map
|
||||||
7
frontend/.vite/deps/react.js.map
Normal file
7
frontend/.vite/deps/react.js.map
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
9
frontend/README.md
Normal file
9
frontend/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/index.html
Normal file
16
frontend/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>
|
||||||
4536
frontend/package-lock.json
generated
Normal file
4536
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
frontend/package.json
Normal file
37
frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
frontend/public/1.jpg
Normal file
BIN
frontend/public/1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
BIN
frontend/public/10.jpg
Normal file
BIN
frontend/public/10.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
BIN
frontend/public/2.jpg
Normal file
BIN
frontend/public/2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
BIN
frontend/public/3.jpg
Normal file
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
Loading…
Reference in New Issue
Block a user