diff --git a/.env b/.env new file mode 100644 index 0000000..73f2ff0 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +JWT_SECRET=a4f6c1f6b9b5c1c7c3a63cce9f9c5b7d6f8e3c7c2a0f7a1d9e0b4c8d2a1f5e6c3b2d9f0a6e7c4b1d8f3a2c5e6d9b7a0 +CDN_BASE_URL=http://localhost:3500 +CDN_SERVICE_TOKEN=5075761248974997bf50fdf2a136e2d27320c15358d59ab09e37438b3ee7f08e1d15770a114a8774bf90e14189560832 \ No newline at end of file diff --git a/.env.development b/.env.development index 5577c3f..73f2ff0 100644 --- a/.env.development +++ b/.env.development @@ -1 +1,3 @@ -JWT_SECRET=a4f6c1f6b9b5c1c7c3a63cce9f9c5b7d6f8e3c7c2a0f7a1d9e0b4c8d2a1f5e6c3b2d9f0a6e7c4b1d8f3a2c5e6d9b7a0 \ No newline at end of file +JWT_SECRET=a4f6c1f6b9b5c1c7c3a63cce9f9c5b7d6f8e3c7c2a0f7a1d9e0b4c8d2a1f5e6c3b2d9f0a6e7c4b1d8f3a2c5e6d9b7a0 +CDN_BASE_URL=http://localhost:3500 +CDN_SERVICE_TOKEN=5075761248974997bf50fdf2a136e2d27320c15358d59ab09e37438b3ee7f08e1d15770a114a8774bf90e14189560832 \ No newline at end of file diff --git a/.env.production b/.env.production index 5577c3f..73f2ff0 100644 --- a/.env.production +++ b/.env.production @@ -1 +1,3 @@ -JWT_SECRET=a4f6c1f6b9b5c1c7c3a63cce9f9c5b7d6f8e3c7c2a0f7a1d9e0b4c8d2a1f5e6c3b2d9f0a6e7c4b1d8f3a2c5e6d9b7a0 \ No newline at end of file +JWT_SECRET=a4f6c1f6b9b5c1c7c3a63cce9f9c5b7d6f8e3c7c2a0f7a1d9e0b4c8d2a1f5e6c3b2d9f0a6e7c4b1d8f3a2c5e6d9b7a0 +CDN_BASE_URL=http://localhost:3500 +CDN_SERVICE_TOKEN=5075761248974997bf50fdf2a136e2d27320c15358d59ab09e37438b3ee7f08e1d15770a114a8774bf90e14189560832 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 87f6c76..df95318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,14 @@ "license": "ISC", "dependencies": { "auto-bind": "^4.0.0", + "axios": "^1.16.1", "bcryptjs": "^3.0.3", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "csurf": "^1.11.0", "express": "^5.2.1", "express-rate-limit": "^8.5.2", + "form-data": "^4.0.5", "helmet": "^8.1.0", "hpp": "^0.2.3", "http-errors": "^2.0.1", @@ -32,6 +34,7 @@ "@types/express": "^5.0.6", "@types/hpp": "^0.2.7", "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.1.0", "@types/node": "^25.9.0", "dotenv": "^17.4.2", "nodemon": "^3.1.14", @@ -211,7 +214,6 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -265,12 +267,21 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/multer": { + "version": "2.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "25.9.0", "resolved": "https://package-mirror.liara.ir/repository/npm/@types/node/-/node-25.9.0.tgz", "integrity": "sha512-AOQwYUNolgy3VosiRqXrACUXTN8nJUtPl7FJXMqZVyxiiCLhQuG3jXKvCS1ALr+Y2OmZhzzLVlYPEqJaiqkaJQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -365,6 +376,18 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://package-mirror.liara.ir/repository/npm/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -418,6 +441,12 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://package-mirror.liara.ir/repository/npm/at-least-node/-/at-least-node-1.0.0.tgz", @@ -440,6 +469,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://package-mirror.liara.ir/repository/npm/balanced-match/-/balanced-match-4.0.4.tgz", @@ -726,6 +767,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://package-mirror.liara.ir/repository/npm/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://package-mirror.liara.ir/repository/npm/commander/-/commander-10.0.1.tgz", @@ -971,6 +1024,15 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://package-mirror.liara.ir/repository/npm/depd/-/depd-2.0.0.tgz", @@ -1144,6 +1206,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://package-mirror.liara.ir/repository/npm/escalade/-/escalade-3.2.0.tgz", @@ -1174,7 +1251,6 @@ "resolved": "https://package-mirror.liara.ir/repository/npm/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -1265,6 +1341,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://package-mirror.liara.ir/repository/npm/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1282,6 +1378,43 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://package-mirror.liara.ir/repository/npm/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://package-mirror.liara.ir/repository/npm/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://package-mirror.liara.ir/repository/npm/forwarded/-/forwarded-0.2.0.tgz", @@ -1496,6 +1629,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://package-mirror.liara.ir/repository/npm/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.3", "resolved": "https://package-mirror.liara.ir/repository/npm/hasown/-/hasown-2.0.3.tgz", @@ -1593,6 +1741,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://package-mirror.liara.ir/repository/npm/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://package-mirror.liara.ir/repository/npm/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -2248,7 +2409,6 @@ "resolved": "https://package-mirror.liara.ir/repository/npm/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -2424,6 +2584,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://package-mirror.liara.ir/repository/npm/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://package-mirror.liara.ir/repository/npm/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -3152,7 +3321,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 682c6ae..dbc747d 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,14 @@ "type": "commonjs", "dependencies": { "auto-bind": "^4.0.0", + "axios": "^1.16.1", "bcryptjs": "^3.0.3", "cookie-parser": "^1.4.7", "cors": "^2.8.6", "csurf": "^1.11.0", "express": "^5.2.1", "express-rate-limit": "^8.5.2", + "form-data": "^4.0.5", "helmet": "^8.1.0", "hpp": "^0.2.3", "http-errors": "^2.0.1", @@ -38,6 +40,7 @@ "@types/express": "^5.0.6", "@types/hpp": "^0.2.7", "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.1.0", "@types/node": "^25.9.0", "dotenv": "^17.4.2", "nodemon": "^3.1.14", diff --git a/src/app.ts b/src/app.ts index 389c402..638c9f6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,7 +19,7 @@ const express = require("express") as typeof import("express"); dotenv.config(); export default class ServerApplication { - #PORT = 4000; + #PORT = 5000; #APP = express(); constructor() { this.serverConfiguration(); @@ -47,8 +47,6 @@ export default class ServerApplication { await initDB(); // await seedRBAC(); - - this.#APP.listen(this.#PORT, () => { console.log( `Server Running on PORT ${this.#PORT} url : ${"http://localhost:"}${ @@ -64,11 +62,12 @@ export default class ServerApplication { // this.#APP.get("/", (req, res) => res.send("")); this.#APP.use("/api/v1", mainRouter); - } ErrorHandlingConfiguration() { this.#APP.use((req: any, res: Response, next: NextFunction) => { - next(createHttpError.NotFound("این آدرس یافت نشد")); + console.log("dasdadasd"); + + next(createHttpError.NotFound("این آدرس یافت نشddد")); }); this.#APP.use( async (error: any, req: any, res: ServerResponse, next: NextFunction) => { diff --git a/src/core/constant/index.ts b/src/core/constant/index.ts index f3b7903..d427902 100644 --- a/src/core/constant/index.ts +++ b/src/core/constant/index.ts @@ -1 +1,2 @@ -export const TOKEN_NAME = 'tid' \ No newline at end of file +export const TOKEN_NAME = "tid"; + diff --git a/src/core/middleware/authApplicant.middleware.ts b/src/core/middleware/authApplicant.middleware.ts new file mode 100644 index 0000000..14044e9 --- /dev/null +++ b/src/core/middleware/authApplicant.middleware.ts @@ -0,0 +1,68 @@ +import { Request, NextFunction } from "express"; +import jwt from "jsonwebtoken"; +import { ServerResponse } from "../types"; +import { Applicant } from "../../models/Applicant"; +import { TOKEN_NAME } from "../constant"; + +interface JwtPayload { + userId: string; + iat?: number; + exp?: number; +} + +export const authApplicant = async ( + req: any, // یا استفاده از تیپ سفارشی که قبلاً بحث کردیم + res: ServerResponse, + next: NextFunction, +) => { + try { + const token = req.cookies[TOKEN_NAME]; + + if (!token) { + return res.status(401).json({ + status: 401, + data: {}, + message: "نشست شما منقضی شده است. لطفا دوباره وارد شوید", + }); + } + + // 2. تایید توکن + const decoded = jwt.verify( + token, + process.env.JWT_SECRET || "secret", + ) as JwtPayload; + + // console.log(decoded) + // if (!decoded.userId) { + // return res.status(401).json({ + // status: 401, + // data: {}, + // message: "توکن نامعتبر است", + // }); + // } + + console.log(decoded); + // 3. چک کردن وجود متقاضی در دیتابیس + const applicant = await Applicant.findByPk(decoded.userId); + + // if (!applicant) { + // return res.status(401).json({ + // status: 401, + // data: {}, + // message: "متقاضی یافت نشد", + // }); + // } + + // 4. قرار دادن ID در Request برای استفاده در مراحل بعدی (مانند selectCenter) + req.applicantId = applicant?.id; + + next(); + } catch (error) { + console.log(error) + return res.status(401).json({ + status: 401, + data: {}, + message: "اعتبارنامه نامعتبر است", + }); + } +}; diff --git a/src/core/router/main.router.ts b/src/core/router/main.router.ts index dd54ec7..b5ee5bc 100644 --- a/src/core/router/main.router.ts +++ b/src/core/router/main.router.ts @@ -1,14 +1,15 @@ -import { Router } from 'express'; -import AuthRouter from '../../modules/auth/router/auth.routes'; -import formRouter from '../../modules/forms/index.routes'; -import userRouter from '../../modules/user/routes/user.routes'; -import CenterRouter from '../../modules/forms/center/routes/center.routes'; +import { Router } from "express"; +import AuthRouter from "../../modules/auth/router/auth.routes"; +import formRouter from "../../modules/forms/index.routes"; +import userRouter from "../../modules/user/routes/user.routes"; +import CenterRouter from "../../modules/center/routes/center.routes"; +import fileRouter from "../../modules/file/routes/file.routes"; const mainRouter = Router(); - -mainRouter.use('/auth',AuthRouter) -mainRouter.use('/user',userRouter) -mainRouter.use('/form',formRouter) -mainRouter.use('/center',CenterRouter) -export default mainRouter; \ No newline at end of file +mainRouter.use("/auth", AuthRouter); +mainRouter.use("/user", userRouter); +mainRouter.use("/form", formRouter); +mainRouter.use("/center", CenterRouter); +mainRouter.use("/files", fileRouter); +export default mainRouter; diff --git a/src/models/Applicant.ts b/src/models/Applicant.ts index 266396f..6e18962 100644 --- a/src/models/Applicant.ts +++ b/src/models/Applicant.ts @@ -2,22 +2,26 @@ import { Model, DataTypes, Sequelize, Optional } from "sequelize"; export interface ApplicantAttributes { id: string; - isCompleted:boolean, - formStep:number, + centerId?: string | null; + isCompleted: boolean; + formStep: number; createdAt?: Date; updatedAt?: Date; } -export interface ApplicantCreationAttributes - extends Optional {} +export interface ApplicantCreationAttributes extends Optional< + ApplicantAttributes, + "id" | "isCompleted" | "formStep" | "createdAt" | "updatedAt" +> {} export class Applicant extends Model implements ApplicantAttributes { public id!: string; + public centerId?: string | null; public isCompleted!: boolean; - public formStep!:number; + public formStep!: number; public readonly createdAt!: Date; public readonly updatedAt!: Date; @@ -30,22 +34,30 @@ export class Applicant primaryKey: true, allowNull: false, }, - isCompleted:{ - type:DataTypes.BOOLEAN, - allowNull:false, - defaultValue:false + centerId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: "centers", + key: "id", + }, + }, + isCompleted: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + formStep: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 1, }, - formStep:{ - type:DataTypes.INTEGER, - allowNull:false, - defaultValue:0 - } }, { sequelize, tableName: "applicants", - timestamps: true - } + timestamps: true, + }, ); return Applicant; diff --git a/src/models/Identity.ts b/src/models/Identity.ts index f1009e0..4d7cf90 100644 --- a/src/models/Identity.ts +++ b/src/models/Identity.ts @@ -1,4 +1,5 @@ import { Model, DataTypes, Sequelize, Optional } from "sequelize"; +import { Applicant } from "./Applicant"; export interface IdentityAttributes { id: string; @@ -45,6 +46,7 @@ export class Identity public gender!: string; public religion!: string; public nationality!: string; + public readonly applicant?: Applicant; public profilePhotoId?: string; diff --git a/src/models/index.ts b/src/models/index.ts index c2c2c08..cb6fc77 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -51,6 +51,9 @@ const models = { JobRequest: JobRequest.initModel(sequelize), Job: Job.initModel(sequelize), }; + +Applicant.belongsTo(Center, { foreignKey: "centerId", as: "center" }); +Center.hasMany(Applicant, { foreignKey: "centerId", as: "applicants" }); // Role.hasMany(User, { foreignKey: "roleId" }); // User.belongsTo(Role, { foreignKey: "roleId" }); diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 8971653..299e690 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -36,9 +36,7 @@ class AuthControllerClass extends Controller { } async applicantLogin(req: any, res: ServerResponse, next: NextFunction) { try { - const data = await this.#service.applicantLogin( - req?.body?.nationalCode, - ); + const data = await this.#service.applicantLogin(req?.body?.nationalCode); res.cookie(TOKEN_NAME, data.token, { httpOnly: true, secure: false, @@ -47,7 +45,13 @@ class AuthControllerClass extends Controller { }); return res.status(200).json({ status: 200, - data, + data: { + applicant: { + id: data.applicant.id, + formStep: data?.applicant?.formStep || 1, // <--- این را اضافه کنید + centerId: data?.applicant?.centerId, // <--- برای تکمیل دیتای فرانت + }, + }, message: "Ok", }); } catch (error) { diff --git a/src/modules/auth/service/auth.service.ts b/src/modules/auth/service/auth.service.ts index 31bccba..8d59179 100644 --- a/src/modules/auth/service/auth.service.ts +++ b/src/modules/auth/service/auth.service.ts @@ -57,21 +57,44 @@ class AuthServiceClass extends Controller { ], }); - + let applicant = identity?.applicant; + + // // اگر identity وجود داشت ولی applicant نداشت + // if (identity && !applicant) { + // applicant = await Applicant.create({ + // formStep: 1, + // isCompleted: false, + // centerId: null, // باید nullable باشد + // }); + + // await identity.update({ + // applicantId: applicant.id, + // }); + // } + + // // اگر identity اصلاً وجود نداشت => applicant جدید بساز + // if (!identity) { + // applicant = await Applicant.create({ + // formStep: 1, + // isCompleted: false, + // centerId: null, // باید nullable باشد + // }); + // } const token = jwt.sign( - { userId: identity?.applicantId }, + { userId: applicant?.id }, process.env.JWT_SECRET || "secret", - { expiresIn: "24h" }, // طول عمر توکن + { expiresIn: "24h" }, ); return { token, applicant: { - id: identity?.applicantId, - fullname: `${identity?.firstName} ${identity?.lastName}`, - role: identity, + id: identity?.applicant?.id, + formStep: identity?.applicant?.formStep || 1, + centerId: identity?.applicant?.centerId ?? null, }, + isNewUser: !identity, }; } catch (error) { throw error; diff --git a/src/modules/forms/center/controller/center.controller.ts b/src/modules/center/controller/center.controller.ts similarity index 80% rename from src/modules/forms/center/controller/center.controller.ts rename to src/modules/center/controller/center.controller.ts index 58db448..eb3bf29 100644 --- a/src/modules/forms/center/controller/center.controller.ts +++ b/src/modules/center/controller/center.controller.ts @@ -1,6 +1,6 @@ import { NextFunction } from "express"; -import { Controller } from "../../../../core/controller/main.controller"; -import { ServerResponse } from "../../../../core/types"; +import { Controller } from "../../../core/controller/main.controller"; +import { ServerResponse } from "../../../core/types"; import CenterService from "../service/center.service"; class CenterControllerClass extends Controller { @@ -74,6 +74,18 @@ class CenterControllerClass extends Controller { next(error); } } + async selectCenter(req: any, res: ServerResponse, next: NextFunction) { + try { + await this.#service.selectCenter(req.body || {}, req?.applicantId ?? ""); + return res.status(200).json({ + status: 200, + data: {}, + message: "ايجاد شد", + }); + } catch (error) { + next(error); + } + } } const CenterController = new CenterControllerClass(); diff --git a/src/modules/center/routes/center.routes.ts b/src/modules/center/routes/center.routes.ts new file mode 100644 index 0000000..475fb8e --- /dev/null +++ b/src/modules/center/routes/center.routes.ts @@ -0,0 +1,16 @@ +import { Router } from "express"; +import CenterController from "../controller/center.controller"; +import { requireAuth } from "../../../core/middleware/auth.middleware"; +import { authApplicant } from "../../../core/middleware/authApplicant.middleware"; + +const CenterRouter = Router(); + +CenterRouter.post("/select", authApplicant, CenterController.selectCenter); + +CenterRouter.post("/create", requireAuth, CenterController.create); // ایجاد +CenterRouter.get("/all", CenterController.getAll); // لیست همه +CenterRouter.get("/get/:id", requireAuth, CenterController.getById); // مشاهده یکی +CenterRouter.put("/update/:id", requireAuth, CenterController.update); // ویرایش +CenterRouter.delete("/delete/:id", requireAuth, CenterController.delete); // حذف + +export default CenterRouter; diff --git a/src/modules/forms/center/service/center.service.ts b/src/modules/center/service/center.service.ts similarity index 54% rename from src/modules/forms/center/service/center.service.ts rename to src/modules/center/service/center.service.ts index 2750e93..63202df 100644 --- a/src/modules/forms/center/service/center.service.ts +++ b/src/modules/center/service/center.service.ts @@ -1,6 +1,10 @@ -import { Controller } from "../../../../core/controller/main.controller"; -import { Center } from "../../../../models/Center"; -import { JobCategory } from "../../../../models/JobCategory"; +import { UUIDV4 } from "sequelize"; +import { Controller } from "../../../core/controller/main.controller"; +import { Applicant } from "../../../models/Applicant"; +import { Center } from "../../../models/Center"; +import { JobCategory } from "../../../models/JobCategory"; +import createHttpError from "http-errors"; +import { GlobalErrorMessages } from "../../../core/messages/errors"; class CenterServiceClass extends Controller { // ایجاد مرکز جدید @@ -8,6 +12,33 @@ class CenterServiceClass extends Controller { return await Center.create(data); } + async selectCenter(data: { centerId: string },applicantId:string) { + try { + const applicant = await Applicant.findByPk( applicantId); + + if (!applicant) { + throw new createHttpError.NotFound("متقاضی یافت نشد"); + } + + applicant.centerId = data.centerId; + applicant.formStep = 2; + applicant.isCompleted = false; + + await applicant.save(); + + return { + id: applicant.id, + centerId: applicant.centerId, + formStep: applicant.formStep, + isCompleted: applicant.isCompleted, + }; + } catch (error) { + console.log(error); + throw new createHttpError.InternalServerError( + GlobalErrorMessages.server.internal, + ); + } + } // دریافت لیست تمام مراکز (همراه با رسته‌های شغلی مرتبط) async getAll() { return await Center.findAll({ diff --git a/src/modules/file/controller/file.controller.ts b/src/modules/file/controller/file.controller.ts index e69de29..d18b112 100644 --- a/src/modules/file/controller/file.controller.ts +++ b/src/modules/file/controller/file.controller.ts @@ -0,0 +1,80 @@ +// src/controllers/file.controller.ts +import { Request, Response } from "express"; +import multer from "multer"; +import { uploadFileToCDN } from "../service/file.service"; +import axios from "axios"; +import { File } from "../../../models/File"; + +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 2 * 1024 * 1024, // 2MB + }, +}); + +export const uploadMiddleware = upload.single("file"); + +export const uploadFileController = async (req: Request, res: Response) => { + try { + if (!req.file) { + return res.status(400).json({ message: "فایلی ارسال نشده است" }); + } + + const result = await uploadFileToCDN( + req.file.buffer, + req.file.originalname, + req.file.mimetype, + ); + + return res.status(201).json(result); + } catch (error: any) { + console.error("Upload Error:", error?.response?.data || error.message); + + return res.status(500).json({ + message: "خطا در آپلود فایل", + error: error?.response?.data || error.message, + }); + } +}; +export const deleteFileController = async (req: Request, res: Response) => { + try { + const { fileId } = req.body; + + if (!fileId) { + return res.status(400).json({ message: "fileId is required" }); + } + + const file = await File.findByPk(fileId); + + if (!file) { + return res.status(404).json({ message: "File not found" }); + } + + console.log(`${process.env.CDN_BASE_URL}/upload/delete`); + + await axios.post( + `${process.env.CDN_BASE_URL}/upload/delete`, + { fileUrl: file.path }, + { + headers: { + Authorization: `Bearer ${process.env.CDN_SERVICE_TOKEN}`, + "Content-Type": "application/json", + }, + }, + ); + + await file.destroy(); + + return res.json({ success: true }); + } catch (error: any) { + console.error( + "deleteFileController error:", + error?.response?.data || error.message, + ); + + return res.status(error?.response?.status || 500).json({ + message: "Error deleting file", + error: error?.response?.data || error.message, + }); + } +}; diff --git a/src/modules/file/routes/file.routes.ts b/src/modules/file/routes/file.routes.ts new file mode 100644 index 0000000..d6fe23b --- /dev/null +++ b/src/modules/file/routes/file.routes.ts @@ -0,0 +1,11 @@ +// src/routes/file.routes.ts +import { Router } from "express"; +import { deleteFileController, uploadFileController, uploadMiddleware } from "../controller/file.controller"; + + +const fileRouter = Router(); + +fileRouter.post("/upload", uploadMiddleware, uploadFileController); +fileRouter.post("/delete", uploadMiddleware, deleteFileController); + +export default fileRouter; diff --git a/src/modules/file/service/file.service.ts b/src/modules/file/service/file.service.ts index e69de29..e1ea5b7 100644 --- a/src/modules/file/service/file.service.ts +++ b/src/modules/file/service/file.service.ts @@ -0,0 +1,58 @@ +// src/services/file.service.ts +import axios from "axios"; +import FormData from "form-data"; +import { File } from "../../../models/File"; +require("dotenv").config(); + +interface UploadResult { + id: string; + url: string; + size: number; + mimeType: string; +} + +export const uploadFileToCDN = async ( + fileBuffer: Buffer, + originalName: string, + mimeType: string, +): Promise => { + const formData = new FormData(); + formData.append("file", fileBuffer, { + filename: originalName, + contentType: mimeType, + }); + + console.log(`Bearer ${process.env.CDN_SERVICE_TOKEN}`); + // 1️⃣ ارسال به CDN + const response = await axios.post(`http://localhost:3500/upload`, formData, { + headers: { + ...formData.getHeaders(), + Authorization: `Bearer ${process.env.CDN_SERVICE_TOKEN}`, + }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + }); + + console.log(response); + + const { url, size, mime } = response.data; + + // 2️⃣ ذخیره در دیتابیس + const createdFile = await File.create({ + fileName: url.split("/").pop()!, + originalName, + mimeType: mime, + + path: url, + size, + }); + + console.log(url); + return { + id: createdFile.id, + url, + + size, + mimeType: mime, + }; +}; diff --git a/src/modules/forms/center/routes/center.routes.ts b/src/modules/forms/center/routes/center.routes.ts deleted file mode 100644 index b46ac0c..0000000 --- a/src/modules/forms/center/routes/center.routes.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Router } from "express"; -import CenterController from "../controller/center.controller"; - -const CenterRouter = Router(); - -CenterRouter.post("/", CenterController.create); // ایجاد -CenterRouter.get("/all", CenterController.getAll); // لیست همه -CenterRouter.get("/get/:id", CenterController.getById); // مشاهده یکی -CenterRouter.put("/update/:id", CenterController.update); // ویرایش -CenterRouter.delete("/delete/:id", CenterController.delete); // حذف - -export default CenterRouter; diff --git a/src/modules/forms/identity/controller/identityForm.controller.ts b/src/modules/forms/identity/controller/identityForm.controller.ts index 3c866e5..ff29a35 100644 --- a/src/modules/forms/identity/controller/identityForm.controller.ts +++ b/src/modules/forms/identity/controller/identityForm.controller.ts @@ -27,7 +27,8 @@ class IdentityFormControllerClass extends Controller { // create and import identity form (step 1 in system) async create(req: any, res: ServerResponse, next: NextFunction) { try { - await this.#service.create(req.body); + const applicantId = req?.applicantId; + await this.#service.create(req.body, applicantId); return res.status(200).json({ status: 200, @@ -53,7 +54,6 @@ class IdentityFormControllerClass extends Controller { next(error); } } - } const IdentityFormController = new IdentityFormControllerClass(); diff --git a/src/modules/forms/identity/index.routes.ts b/src/modules/forms/identity/index.routes.ts index c3f95f8..61863fa 100644 --- a/src/modules/forms/identity/index.routes.ts +++ b/src/modules/forms/identity/index.routes.ts @@ -1,9 +1,10 @@ import { Router } from "express"; import IdentityFormController from "./controller/identityForm.controller"; +import { authApplicant } from "../../../core/middleware/authApplicant.middleware"; const identityRouter = Router(); -identityRouter.post("/create", IdentityFormController.create); +identityRouter.post("/create", authApplicant,IdentityFormController.create); // identityRouter.get("/all", IdentityFormController.getAll); identityRouter.get("/get/:id", IdentityFormController.getById); identityRouter.put("/update/:id", IdentityFormController.update); diff --git a/src/modules/forms/identity/service/identityForm.service.ts b/src/modules/forms/identity/service/identityForm.service.ts index f745147..fd78097 100644 --- a/src/modules/forms/identity/service/identityForm.service.ts +++ b/src/modules/forms/identity/service/identityForm.service.ts @@ -18,37 +18,67 @@ class IdentityFormServiceClass extends Controller { ); } } - async create(data: any) { - let applicantId = null; - - try { - applicantId = await Applicant.create({ - id: UUIDV4().toString({}), - }); - } catch (error) { - throw new createHttpError.InternalServerError( - GlobalErrorMessages.server.internal, - ); - } - - try { - await Identity.create({ - applicantId: applicantId.id, - birthDate: data.birthDate, - birthPlace: data.birthPlace, - fatherName: data.fatherName, - firstName: data.firstName, - nationalCode: data.nationalCode, - gender: data.gender, - lastName: data.lastName, - nationality: data.nationality, - religion: data.religion, - profilePhotoId: data.profilePhotoId, - }); - } catch (error) { - throw new createHttpError.InternalServerError( - GlobalErrorMessages.server.internal, - ); + async create(data: any, applicantId: string) { + if (!applicantId) { + let newApplicant; + try { + newApplicant = await Applicant.create({ + centerId: data?.centerId, + formStep: 3, + isCompleted: false, + }); + } catch (error) { + throw new createHttpError.InternalServerError( + GlobalErrorMessages.server.internal, + ); + } + try { + await Identity.create({ + applicantId: newApplicant.id, + birthDate: data.birthDate, + birthPlace: data.birthPlace, + fatherName: data.fatherName, + firstName: data.firstName, + nationalCode: data.nationalCode, + gender: data.gender, + lastName: data.lastName, + nationality: data.nationality, + religion: data.religion, + profilePhotoId: data.profilePhotoId, + }); + } catch (error) { + console.log(error); + throw new createHttpError.InternalServerError( + GlobalErrorMessages.server.internal, + ); + } + } else { + try { + await Identity.update( + { + birthDate: data.birthDate, + birthPlace: data.birthPlace, + fatherName: data.fatherName, + firstName: data.firstName, + nationalCode: data.nationalCode, + gender: data.gender, + lastName: data.lastName, + nationality: data.nationality, + religion: data.religion, + profilePhotoId: data.profilePhotoId, + }, + { + where: { + applicantId, + }, + }, + ); + } catch (error) { + console.log(error); + throw new createHttpError.InternalServerError( + GlobalErrorMessages.server.internal, + ); + } } } async update(formID: string, data: any) { diff --git a/src/modules/forms/index.routes.ts b/src/modules/forms/index.routes.ts index 2002ccc..7231ee5 100644 --- a/src/modules/forms/index.routes.ts +++ b/src/modules/forms/index.routes.ts @@ -1,10 +1,10 @@ -import {Router} from "express"; +import { Router } from "express"; import PersonalInfoRouter from "./personalInfo/index.routes"; import identityRouter from "./identity/index.routes"; import PhysicalInfoRouter from "./physicalInfo/index.routes"; import EducationInfoRouter from "./education/index.routes"; import CourseInfoRouter from "./course/index.routes"; -import CenterRouter from "./center/routes/center.routes"; +import CenterRouter from "../center/routes/center.routes"; import referralRouter from "./referral/routes/referral.routes"; import JobRequestRouter from "./jobRequest/routes/jobRequest.routes"; import computerSkillRouter from "./computerSkill/routes/computerSkill.routes"; @@ -13,12 +13,12 @@ import workExperienceRouter from "./workExperience/routes/workExperience.routes" const formRouter = Router(); +formRouter.use("/select-center", CenterRouter); formRouter.use("/identity", identityRouter); formRouter.use("/personal-info", PersonalInfoRouter); formRouter.use("/physical-info", PhysicalInfoRouter); formRouter.use("/education", EducationInfoRouter); formRouter.use("/course", CourseInfoRouter); -formRouter.use("/center", CenterRouter); formRouter.use("/referral", referralRouter); formRouter.use("/job-request", JobRequestRouter); formRouter.use("/computer-skill", computerSkillRouter);