diff --git a/backend/package-lock.json b/backend/package-lock.json index 8a0cddd..3e8fb98 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,7 @@ "@apollo/server": "^4.10.2", "apollo-server-errors": "^3.3.1", "argon2": "^0.40.3", + "axios": "^1.7.7", "class-validator": "^0.14.1", "cookie": "^0.6.0", "cors": "^2.8.5", @@ -866,6 +867,16 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1443,6 +1454,25 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -2362,6 +2392,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", diff --git a/backend/package.json b/backend/package.json index e978d2c..cdd2fd1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "@apollo/server": "^4.10.2", "apollo-server-errors": "^3.3.1", "argon2": "^0.40.3", + "axios": "^1.7.7", "class-validator": "^0.14.1", "cookie": "^0.6.0", "cors": "^2.8.5", diff --git a/backend/src/entities/file.ts b/backend/src/entities/file.ts index ac0303f..d0e1fdc 100644 --- a/backend/src/entities/file.ts +++ b/backend/src/entities/file.ts @@ -14,9 +14,9 @@ import {Upload} from "./upload"; import {Report} from "./report"; import {UserAccessFile} from "./userAccessFile"; -enum StatusOption { - status1 = "status1", - status2 = "status2" +export enum StatusOption { + status1 = "public", + status2 = "private" } @ObjectType() @@ -47,8 +47,8 @@ export class File extends BaseEntity { size: number; @Field() - @Column({type: "enum", enum: StatusOption, nullable: true, default: StatusOption.status1}) - status: StatusOption; + @Column({nullable: true, default: StatusOption.status1}) + privacy_status: StatusOption; @Field() @Column({type: "character varying", nullable: true}) @@ -63,7 +63,6 @@ export class File extends BaseEntity { updated_at: Date; @ManyToOne(() => Upload, (upload) => upload.files) - @JoinColumn({name: 'upload_id'}) upload: Upload; @OneToMany(() => Report, (report) => report.file) diff --git a/backend/src/entities/upload.ts b/backend/src/entities/upload.ts index 3774106..b2175aa 100644 --- a/backend/src/entities/upload.ts +++ b/backend/src/entities/upload.ts @@ -30,7 +30,7 @@ export class Upload extends BaseEntity { message: string @Field() - @Column({type: 'boolean', nullable: false, default: false}) + @Column({type: 'boolean', nullable: false, default: true}) is_activated: boolean; @Field(() => [String]) @@ -51,6 +51,7 @@ export class Upload extends BaseEntity { @ManyToOne(() => User, user => user.uploads) user: User + @Field(() => [File]) @OneToMany(() => File, (file) => file.upload) files: File[]; } diff --git a/backend/src/resolvers/FileResolver.ts b/backend/src/resolvers/FileResolver.ts index 0b398c6..283da8e 100644 --- a/backend/src/resolvers/FileResolver.ts +++ b/backend/src/resolvers/FileResolver.ts @@ -1,5 +1,8 @@ import { File } from "../entities/file"; import { Query, Resolver } from "type-graphql"; +import { Arg, Mutation } from "type-graphql"; +import axios from "axios"; +import { StatusOption } from "../entities/file"; @Resolver(File) class FileResolver { @@ -7,6 +10,79 @@ class FileResolver { async getAllFile() { return await File.find(); } + + @Mutation(() => Boolean) + async deleteFile(@Arg("id") id: number) { + try { + const file = await File.findOneByOrFail({ id }); + + if (!file) { + throw new Error("File not found"); + } + + const response = await axios.delete( + `http://files:3000/files/delete?filename=${file.default_name}` + ); + + if (response.status !== 200) { + throw new Error("Internal server error during file deletion 1"); + } + + await File.delete({ id }); + + return true; + } catch (err) { + throw new Error("Internal server error during file deletion 2"); + } + } + + @Mutation(() => Boolean) + async editFileName( + @Arg("id") id: number, + @Arg("newName") newName: string + ) { + try { + const file = await File.findOneByOrFail({ id }); + + if (!file) { + throw new Error("File not found"); + } + + file.name = newName; + + await file.save(); + + return true; + } catch (err) { + throw new Error("Internal server error during file name edit"); + } + } + + @Mutation(() => String) + async changePrivacyStatus( + @Arg("id") id: number, + @Arg("status") status: string + ) { + try { + const file = await File.findOneByOrFail({ id }); + + if (!file) { + throw new Error("File not found"); + } + + if (!Object.values(StatusOption).includes(status as StatusOption)) { + throw new Error("Invalid status option"); + } + + file.privacy_status = status as StatusOption; + + await file.save(); + + return true; + } catch (err) { + throw new Error("Internal server error during file privacy status"); + } + } } export default FileResolver; diff --git a/backend/src/resolvers/UploadResolver.ts b/backend/src/resolvers/UploadResolver.ts index b1d4869..9607e44 100644 --- a/backend/src/resolvers/UploadResolver.ts +++ b/backend/src/resolvers/UploadResolver.ts @@ -12,6 +12,44 @@ class UploadResolver { return await Upload.find(); } + @Query(() => [Upload]) + async getUploadsByUserId(@Arg("userId") userId: number) { + const user = await User.findOneByOrFail({ id: userId }); + + if (!user) { + throw new Error("User not found"); + } + + try { + const result = await Upload.find({ + where: { user: { id: userId } }, + relations: ["files"], + }); + + return result; + } catch (err) { + throw new Error("Internal server error"); + } + } + + @Mutation(() => String) + async changeUploadActivatedStatus(@Arg("uploadId") uploadId: number) { + const upload = await Upload.findOneByOrFail({ id: uploadId }); + + if (!upload) { + throw new Error("Upload not found"); + } + + upload.is_activated = !upload.is_activated; + + try { + await upload.save(); + return `Upload ${upload.is_activated ? "activated" : "deactivated"}`; + } catch (err) { + throw new Error("Internal server error"); + } + } + @Mutation(() => String) async createUpload( @Arg("receiversEmails", () => [String]) receivers: string[], @@ -28,12 +66,12 @@ class UploadResolver { for (const file of parsedFiles) { const newFile = await File.create({ - name: file.original_name, - size: file.size, - default_name: file.default_name, - type: file.mimetype, - path: file.path, - file_uid: file.uuid, + name: file.original_name, + size: file.size, + default_name: file.default_name, + type: file.mimetype, + path: file.path, + file_uid: file.uuid, }).save(); uploadFiles.push(newFile); @@ -49,12 +87,12 @@ class UploadResolver { if (newUpload) { const downloadToken = createDownloadToken( - { - uploadId: newUpload.id, - receivers, - senderEmail: user.email, - }, - "1h" + { + uploadId: newUpload.id, + receivers, + senderEmail: user.email, + }, + "1h" ); const downloadLink: string = generateDownloadLink(downloadToken); @@ -70,15 +108,15 @@ class UploadResolver { } const userOrVisitor = async (email: string): Promise => { - let user: User | null = await User.findOneBy({ email }); - if (user) return user; + let user: User | null = await User.findOneBy({ email }); + if (user) return user; - let visitor: Visitor | null = await Visitor.findOneBy({ email }); - if (!visitor) { - visitor = await Visitor.create({ email }).save(); - } + let visitor: Visitor | null = await Visitor.findOneBy({ email }); + if (!visitor) { + visitor = await Visitor.create({ email }).save(); + } - return visitor; + return visitor; }; export default UploadResolver; \ No newline at end of file diff --git a/backend/src/resolvers/UserResolver.ts b/backend/src/resolvers/UserResolver.ts index 4198144..17ba32a 100644 --- a/backend/src/resolvers/UserResolver.ts +++ b/backend/src/resolvers/UserResolver.ts @@ -134,9 +134,9 @@ class UserResolver { } @Query(() => [File]) - async getUserFiles(@Arg("userId") userId: string) { + async getUserFiles(@Arg("userId") userId: number) { const user = await User.findOne({ - where: { id: Number(userId) }, + where: { id: userId }, relations: ['uploads', 'uploads.files'], }); @@ -151,7 +151,7 @@ class UserResolver { return acc; }, [] as File[]); - return allFiles; + return allFiles ? allFiles : []; } } diff --git a/files/src/controllers/filesController.ts b/files/src/controllers/filesController.ts index 41c6963..042b111 100644 --- a/files/src/controllers/filesController.ts +++ b/files/src/controllers/filesController.ts @@ -107,6 +107,7 @@ export const addNewUpload = async (req: Request, res: any) => { } }); }; + export const deleteFile = async (req: Request, res: any) => { const filename = req.query.filename @@ -121,6 +122,7 @@ export const deleteFile = async (req: Request, res: any) => { return res.status(404).send(`File not found.`); } + // fs.unlinkSync deletes the file fs.unlinkSync(filePath); return res.status(200).send("File deleted."); diff --git a/files/src/index.ts b/files/src/index.ts index 9755957..0376f54 100644 --- a/files/src/index.ts +++ b/files/src/index.ts @@ -5,9 +5,8 @@ import router from "./router"; const app = express(); const port = 3000; - app.use(cors({ - origin: ["http://localhost:7002", "http://localhost:3000", "http://localhost:5173"], + origin: ["http://localhost:7002", "http://localhost:3000", "http://localhost:5173", "http://localhost:4000"], credentials: true, methods: ["POST", "GET", "DELETE", "PUT", "OPTIONS"], allowedHeaders: ["Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization"] diff --git a/files/src/router/index.ts b/files/src/router/index.ts index af5d341..e5cb0cb 100644 --- a/files/src/router/index.ts +++ b/files/src/router/index.ts @@ -1,8 +1,10 @@ import express from "express"; -import {addNewUpload} from "../controllers/filesController"; +import {addNewUpload, deleteFile} from "../controllers/filesController"; const router = express.Router() router.post('/upload', addNewUpload) +router.delete('/delete', deleteFile) + export default router \ No newline at end of file