docta-package 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +36 -0
- package/src/config.ts +25 -0
- package/src/dto/input/doctor.ts +120 -0
- package/src/dto/input/education.ts +11 -0
- package/src/dto/input/faq.ts +11 -0
- package/src/dto/input/index.ts +9 -0
- package/src/dto/input/language.ts +13 -0
- package/src/dto/input/location.ts +31 -0
- package/src/dto/input/patient.ts +17 -0
- package/src/dto/input/position.ts +21 -0
- package/src/dto/input/specialty.ts +40 -0
- package/src/dto/input/user.ts +87 -0
- package/src/dto/output/doctor.ts +90 -0
- package/src/dto/output/education.ts +11 -0
- package/src/dto/output/faq.ts +11 -0
- package/src/dto/output/index.ts +9 -0
- package/src/dto/output/language.ts +11 -0
- package/src/dto/output/location.ts +21 -0
- package/src/dto/output/patient.ts +49 -0
- package/src/dto/output/position.ts +15 -0
- package/src/dto/output/specialty.ts +53 -0
- package/src/dto/output/user.ts +57 -0
- package/src/enums/gender.ts +4 -0
- package/src/enums/index.ts +4 -0
- package/src/enums/language-levels.ts +9 -0
- package/src/enums/status-codes.ts +28 -0
- package/src/enums/user-role.ts +5 -0
- package/src/errors/BadRequestError.ts +18 -0
- package/src/errors/CustomError.ts +18 -0
- package/src/errors/NotFoundError.ts +18 -0
- package/src/errors/UnAuthorizedError.ts +18 -0
- package/src/errors/index.ts +4 -0
- package/src/index.ts +19 -0
- package/src/interfaces/LoggedInUserToken.ts +9 -0
- package/src/interfaces/index.ts +1 -0
- package/src/middleware/errorHandler.ts +31 -0
- package/src/middleware/index.ts +5 -0
- package/src/middleware/multer.ts +74 -0
- package/src/middleware/require-auth.ts +46 -0
- package/src/middleware/validate-request.ts +40 -0
- package/src/middleware/verify-roles.ts +17 -0
- package/src/models/base.ts +52 -0
- package/src/models/doctor.ts +96 -0
- package/src/models/education.ts +14 -0
- package/src/models/faq.ts +14 -0
- package/src/models/index.ts +10 -0
- package/src/models/language.ts +20 -0
- package/src/models/location.ts +24 -0
- package/src/models/patient.ts +35 -0
- package/src/models/position.ts +19 -0
- package/src/models/specialty.ts +37 -0
- package/src/models/user.ts +67 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/orchestration-result.ts +74 -0
- package/src/utils/s3-helper.ts +72 -0
- package/src/utils/token-utils.ts +86 -0
- package/src/utils/validate-info.ts +26 -0
- package/src/utils/winston.ts +33 -0
- package/tsconfig.json +120 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ISpecialtyDocument } from "../../models";
|
|
2
|
+
import { UserOutputDto } from ".";
|
|
3
|
+
|
|
4
|
+
// Base DTO for everyone
|
|
5
|
+
export class SpecialtyOutputDto {
|
|
6
|
+
id: string;
|
|
7
|
+
en: { name: string; description: string | null };
|
|
8
|
+
fr: { name: string; description: string | null } | null;
|
|
9
|
+
|
|
10
|
+
isDeleted: boolean;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
updatedAt: number;
|
|
13
|
+
|
|
14
|
+
constructor(specialty: ISpecialtyDocument) {
|
|
15
|
+
this.id = specialty.id.toString();
|
|
16
|
+
this.en = {
|
|
17
|
+
name: specialty.en.name,
|
|
18
|
+
description: specialty.en.description || null,
|
|
19
|
+
};
|
|
20
|
+
this.fr = specialty.fr
|
|
21
|
+
? {
|
|
22
|
+
name: specialty.fr.name,
|
|
23
|
+
description: specialty.fr.description || null,
|
|
24
|
+
}
|
|
25
|
+
: null;
|
|
26
|
+
this.isDeleted = specialty.isDeleted;
|
|
27
|
+
this.createdAt = specialty.createdAt;
|
|
28
|
+
this.updatedAt = specialty.updatedAt;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Extended DTO for admin responses
|
|
33
|
+
export class SpecialtyAdminOutputDto extends SpecialtyOutputDto {
|
|
34
|
+
createdBy: UserOutputDto | null;
|
|
35
|
+
updatedBy: UserOutputDto | null;
|
|
36
|
+
deletedBy: UserOutputDto | null;
|
|
37
|
+
|
|
38
|
+
constructor(specialty: ISpecialtyDocument) {
|
|
39
|
+
super(specialty); // call base constructor
|
|
40
|
+
|
|
41
|
+
this.createdBy = specialty.createdBy
|
|
42
|
+
? new UserOutputDto(specialty.createdBy)
|
|
43
|
+
: null;
|
|
44
|
+
|
|
45
|
+
this.updatedBy = specialty.updatedBy
|
|
46
|
+
? new UserOutputDto(specialty.updatedBy)
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
this.deletedBy = specialty.deletedBy
|
|
50
|
+
? new UserOutputDto(specialty.deletedBy)
|
|
51
|
+
: null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { IUserDocument } from "../../models";
|
|
2
|
+
|
|
3
|
+
// Base DTO for everyone
|
|
4
|
+
export class UserOutputDto {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
email: string;
|
|
8
|
+
role: string;
|
|
9
|
+
isActive: boolean;
|
|
10
|
+
isDeleted: boolean;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
updatedAt: number;
|
|
13
|
+
|
|
14
|
+
constructor(user: IUserDocument) {
|
|
15
|
+
this.id = user.id.toString();
|
|
16
|
+
this.name = user.name;
|
|
17
|
+
this.email = user.email;
|
|
18
|
+
this.role = user.role;
|
|
19
|
+
|
|
20
|
+
this.isActive = user.isActive;
|
|
21
|
+
this.isDeleted = user.isDeleted;
|
|
22
|
+
this.createdAt = user.createdAt;
|
|
23
|
+
this.updatedAt = user.updatedAt;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Extended DTO for admin responses
|
|
28
|
+
export class UserAdminOutputDto extends UserOutputDto {
|
|
29
|
+
createdBy: UserOutputDto | null;
|
|
30
|
+
updatedBy: UserOutputDto | null;
|
|
31
|
+
deletedBy: UserOutputDto | null;
|
|
32
|
+
|
|
33
|
+
constructor(user: IUserDocument) {
|
|
34
|
+
super(user); // call base constructor
|
|
35
|
+
|
|
36
|
+
this.createdBy = user.createdBy ? new UserOutputDto(user.createdBy) : null;
|
|
37
|
+
this.updatedBy = user.updatedBy ? new UserOutputDto(user.updatedBy) : null;
|
|
38
|
+
this.deletedBy = user.deletedBy ? new UserOutputDto(user.deletedBy) : null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// DTO for logged-in user responses
|
|
43
|
+
export class LoggedInUserOutputDto {
|
|
44
|
+
user: UserOutputDto | UserAdminOutputDto;
|
|
45
|
+
accessToken: string;
|
|
46
|
+
refreshToken: string;
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
user: UserOutputDto | UserAdminOutputDto,
|
|
50
|
+
accessToken: string,
|
|
51
|
+
refreshToken: string
|
|
52
|
+
) {
|
|
53
|
+
this.user = user;
|
|
54
|
+
this.accessToken = accessToken;
|
|
55
|
+
this.refreshToken = refreshToken;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export enum EnumStatusCode {
|
|
2
|
+
BAD_REQUEST = "BAD_REQUEST",
|
|
3
|
+
SOMETHING_WENT_WRONG = "SOMETHING_WENT_WRONG",
|
|
4
|
+
UNAUTHORIZED = "UNAUTHORIZED",
|
|
5
|
+
VALIDATION_ERROR = "VALIDATION_ERROR",
|
|
6
|
+
EXISTS_ALREADY = "EXISTS_ALREADY",
|
|
7
|
+
CREATED_SUCCESSFULLY = "CREATED_SUCCESSFULLY",
|
|
8
|
+
UPDATED_SUCCESSFULLY = "UPDATED_SUCCESSFULLY",
|
|
9
|
+
NOT_FOUND = "NOT_FOUND",
|
|
10
|
+
SPECIALTY_NOT_FOUND = "SPECIALTY_NOT_FOUND",
|
|
11
|
+
NOT_ALLOWED = "NOT_ALLOWED",
|
|
12
|
+
DOCTOR_NOT_FOUND = "DOCTOR_NOT_FOUND",
|
|
13
|
+
ACCOUNT_DELETED = "ACCOUNT_DELETED",
|
|
14
|
+
ACCOUNT_DEACTIVATED = "ACCOUNT_DEACTIVATED",
|
|
15
|
+
TOKEN_REFRESHED = "TOKEN_REFRESHED",
|
|
16
|
+
|
|
17
|
+
NO_ACCESS_TOKEN = "NO_ACCESS_TOKEN",
|
|
18
|
+
ACCESS_TOKEN_EXPIRED = "ACCESS_TOKEN_EXPIRED",
|
|
19
|
+
CANNOT_DECODE_ACCESS_TOKEN = "CANNOT_DECODE_ACCESS_TOKEN",
|
|
20
|
+
|
|
21
|
+
RECOVERED_SUCCESSFULLY = "RECOVERED_SUCCESSFULLY",
|
|
22
|
+
|
|
23
|
+
FILE_TYPE_MISMATCH = "FILE_TYPE_MISMATCH",
|
|
24
|
+
FILE_TOO_LARGE = "FILE_TOO_LARGE",
|
|
25
|
+
NO_FILE_UPLOADED = "NO_FILE_UPLOADED",
|
|
26
|
+
|
|
27
|
+
DELETED_SUCCESSFULLY = "DELETED_SUCCESSFULLY",
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CustomError } from ".";
|
|
2
|
+
import { EnumStatusCode } from "../enums";
|
|
3
|
+
import { ErrorResult } from "../utils";
|
|
4
|
+
|
|
5
|
+
export class BadRequestError extends CustomError {
|
|
6
|
+
statusCode = 400;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
code: EnumStatusCode = EnumStatusCode.BAD_REQUEST,
|
|
10
|
+
message: string = "Bad Request"
|
|
11
|
+
) {
|
|
12
|
+
super(message, code, 400);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
serializeErrors(): ErrorResult {
|
|
16
|
+
return { code: this.code, message: this.message };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EnumStatusCode } from "../enums";
|
|
2
|
+
import { ErrorResult } from "../utils";
|
|
3
|
+
|
|
4
|
+
export abstract class CustomError extends Error {
|
|
5
|
+
public readonly code: EnumStatusCode;
|
|
6
|
+
public readonly statusCode: number;
|
|
7
|
+
|
|
8
|
+
constructor(message: string, code: EnumStatusCode, statusCode: number) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.statusCode = statusCode;
|
|
12
|
+
|
|
13
|
+
// Maintains proper prototype chain for built-in Error
|
|
14
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
abstract serializeErrors(): ErrorResult;
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CustomError } from ".";
|
|
2
|
+
import { EnumStatusCode } from "../enums";
|
|
3
|
+
import { ErrorResult } from "../utils";
|
|
4
|
+
|
|
5
|
+
export class NotFoundError extends CustomError {
|
|
6
|
+
statusCode = 404;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
code: EnumStatusCode = EnumStatusCode.NOT_FOUND,
|
|
10
|
+
message: string = "Not Found"
|
|
11
|
+
) {
|
|
12
|
+
super(message, code, 404);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
serializeErrors(): ErrorResult {
|
|
16
|
+
return { code: this.code, message: this.message };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CustomError } from ".";
|
|
2
|
+
import { EnumStatusCode } from "../enums";
|
|
3
|
+
import { ErrorResult } from "../utils";
|
|
4
|
+
|
|
5
|
+
export class UnAuthorizedError extends CustomError {
|
|
6
|
+
statusCode = 401;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
code: EnumStatusCode = EnumStatusCode.UNAUTHORIZED,
|
|
10
|
+
message: string = "Unauthorized"
|
|
11
|
+
) {
|
|
12
|
+
super(message, code, 401);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
serializeErrors(): ErrorResult {
|
|
16
|
+
return { code: this.code, message: this.message };
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { LoggedInUserTokenData } from "./interfaces";
|
|
3
|
+
|
|
4
|
+
export * from "./errors";
|
|
5
|
+
export * from "./middleware";
|
|
6
|
+
export * from "./models";
|
|
7
|
+
export * from "./dto/input";
|
|
8
|
+
export * from "./dto/output";
|
|
9
|
+
export * from "./utils";
|
|
10
|
+
export * from "./config";
|
|
11
|
+
export * from "./interfaces";
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
namespace Express {
|
|
15
|
+
interface Request {
|
|
16
|
+
currentUser?: LoggedInUserTokenData;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./LoggedInUserToken";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { CustomError } from "../errors";
|
|
3
|
+
import { EnumStatusCode } from "../enums";
|
|
4
|
+
import { logger } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const errorHandler = (
|
|
7
|
+
err: Error,
|
|
8
|
+
req: Request,
|
|
9
|
+
res: Response,
|
|
10
|
+
next: NextFunction
|
|
11
|
+
): void => {
|
|
12
|
+
if (err instanceof CustomError) {
|
|
13
|
+
res.status(err.statusCode).json(err.serializeErrors());
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Log the error with stack trace and request info
|
|
18
|
+
logger.error("Unhandled error", {
|
|
19
|
+
message: err.message,
|
|
20
|
+
stack: err.stack,
|
|
21
|
+
path: req.originalUrl,
|
|
22
|
+
method: req.method,
|
|
23
|
+
body: req.body,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.error(err);
|
|
27
|
+
res.status(500).json({
|
|
28
|
+
code: EnumStatusCode.SOMETHING_WENT_WRONG,
|
|
29
|
+
message: "Internal Server Error",
|
|
30
|
+
});
|
|
31
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import multer from "multer";
|
|
2
|
+
import { ErrorRequestHandler } from "express";
|
|
3
|
+
import { BadRequestError } from "../errors";
|
|
4
|
+
import { EnumStatusCode } from "../enums";
|
|
5
|
+
|
|
6
|
+
// 5 MB file size limit
|
|
7
|
+
const fileSize = 5 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
// Use memory storage since files are likely forwarded to S3
|
|
10
|
+
const storage = multer.memoryStorage();
|
|
11
|
+
|
|
12
|
+
const allowedTypes = [
|
|
13
|
+
"image/jpeg",
|
|
14
|
+
"image/png",
|
|
15
|
+
"image/gif",
|
|
16
|
+
"image/bmp",
|
|
17
|
+
"image/webp",
|
|
18
|
+
"image/tiff",
|
|
19
|
+
"image/svg+xml",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Base multer instance configured for images
|
|
23
|
+
export const imageUpload = multer({
|
|
24
|
+
storage,
|
|
25
|
+
limits: { fileSize },
|
|
26
|
+
fileFilter(req, file, cb) {
|
|
27
|
+
if (!allowedTypes.includes(file.mimetype)) {
|
|
28
|
+
return cb(
|
|
29
|
+
new BadRequestError(
|
|
30
|
+
EnumStatusCode.FILE_TYPE_MISMATCH,
|
|
31
|
+
"Only image files are allowed!"
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
cb(null, true);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Error transformer to convert Multer errors into our CustomError format
|
|
40
|
+
export const transformMulterError: ErrorRequestHandler = (
|
|
41
|
+
err,
|
|
42
|
+
_req,
|
|
43
|
+
_res,
|
|
44
|
+
next
|
|
45
|
+
) => {
|
|
46
|
+
// Multer throws MulterError for limits and other internal validations
|
|
47
|
+
if (err && (err as any).name === "MulterError") {
|
|
48
|
+
const multerErr = err as multer.MulterError;
|
|
49
|
+
if (multerErr.code === "LIMIT_FILE_SIZE") {
|
|
50
|
+
return next(
|
|
51
|
+
new BadRequestError(
|
|
52
|
+
EnumStatusCode.FILE_TOO_LARGE,
|
|
53
|
+
`File too large. Max ${Math.floor(fileSize / (1024 * 1024))}MB`
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return next(
|
|
58
|
+
new BadRequestError(EnumStatusCode.BAD_REQUEST, multerErr.message)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
// If it's already a CustomError or another error, pass it along
|
|
62
|
+
return next(err);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Convenience helpers to use directly in routers
|
|
66
|
+
export const uploadSingleImage = (fieldName: string) => [
|
|
67
|
+
imageUpload.single(fieldName),
|
|
68
|
+
transformMulterError,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
export const uploadArrayImages = (fieldName: string, maxCount: number) => [
|
|
72
|
+
imageUpload.array(fieldName, maxCount),
|
|
73
|
+
transformMulterError,
|
|
74
|
+
];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { EnumStatusCode } from "../enums";
|
|
3
|
+
import { UnAuthorizedError } from "../errors";
|
|
4
|
+
import { TokenUtils } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const requireAuth = async (
|
|
7
|
+
req: Request,
|
|
8
|
+
res: Response,
|
|
9
|
+
next: NextFunction
|
|
10
|
+
) => {
|
|
11
|
+
const accessToken = req.header("Authorization")?.replace("Bearer ", "");
|
|
12
|
+
|
|
13
|
+
if (!accessToken) {
|
|
14
|
+
throw new UnAuthorizedError(
|
|
15
|
+
EnumStatusCode.NO_ACCESS_TOKEN,
|
|
16
|
+
"No access token"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const decoded = TokenUtils.verifyAccessToken(accessToken);
|
|
22
|
+
|
|
23
|
+
if (!decoded) {
|
|
24
|
+
throw new UnAuthorizedError(
|
|
25
|
+
EnumStatusCode.CANNOT_DECODE_ACCESS_TOKEN,
|
|
26
|
+
"Cannot decode access token"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
req.currentUser = decoded;
|
|
31
|
+
|
|
32
|
+
next();
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
if (error.name === "TokenExpiredError") {
|
|
35
|
+
throw new UnAuthorizedError(
|
|
36
|
+
EnumStatusCode.ACCESS_TOKEN_EXPIRED,
|
|
37
|
+
"Access token has expired."
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
throw new UnAuthorizedError(
|
|
41
|
+
EnumStatusCode.CANNOT_DECODE_ACCESS_TOKEN,
|
|
42
|
+
"Cannot decode access token"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { plainToInstance } from "class-transformer";
|
|
2
|
+
import { validate, ValidationError } from "class-validator";
|
|
3
|
+
import { Request, Response, NextFunction, RequestHandler } from "express";
|
|
4
|
+
import { BadRequestError } from "../errors";
|
|
5
|
+
import { EnumStatusCode } from "../enums";
|
|
6
|
+
|
|
7
|
+
export const validationMiddleware = (
|
|
8
|
+
type: any,
|
|
9
|
+
skipMissingProperties = false,
|
|
10
|
+
whitelist = true,
|
|
11
|
+
forbidNonWhitelisted = true
|
|
12
|
+
): RequestHandler => {
|
|
13
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
14
|
+
try {
|
|
15
|
+
const object = plainToInstance(type, req.body);
|
|
16
|
+
const errors = await validate(object, {
|
|
17
|
+
skipMissingProperties,
|
|
18
|
+
whitelist,
|
|
19
|
+
forbidNonWhitelisted,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (errors.length > 0) {
|
|
23
|
+
const message = errors
|
|
24
|
+
.map((error: ValidationError) =>
|
|
25
|
+
Object.values(error.constraints ?? {})
|
|
26
|
+
)
|
|
27
|
+
.flat()
|
|
28
|
+
.join(", ");
|
|
29
|
+
return next(
|
|
30
|
+
new BadRequestError(EnumStatusCode.VALIDATION_ERROR, message)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
req.body = object;
|
|
35
|
+
next();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
next(err);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { EnumUserRole } from "../enums/user-role";
|
|
3
|
+
import { UnAuthorizedError } from "../errors";
|
|
4
|
+
import { EnumStatusCode } from "../enums";
|
|
5
|
+
|
|
6
|
+
export const verifyRoles = (roles: EnumUserRole[]) => {
|
|
7
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
8
|
+
if (roles.includes(req.currentUser?.role as EnumUserRole)) {
|
|
9
|
+
next();
|
|
10
|
+
} else {
|
|
11
|
+
throw new UnAuthorizedError(
|
|
12
|
+
EnumStatusCode.NOT_ALLOWED,
|
|
13
|
+
"You are not allowed to perform this action."
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Schema, SchemaDefinition } from "mongoose";
|
|
2
|
+
import { IUserDocument } from ".";
|
|
3
|
+
|
|
4
|
+
export interface IBaseModel {
|
|
5
|
+
isDeleted: boolean;
|
|
6
|
+
createdAt: number;
|
|
7
|
+
updatedAt: number;
|
|
8
|
+
deletedAt?: number;
|
|
9
|
+
createdBy?: IUserDocument;
|
|
10
|
+
updatedBy?: IUserDocument;
|
|
11
|
+
deletedBy?: IUserDocument;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Base Schema
|
|
15
|
+
export const BaseSchemaFields: SchemaDefinition = {
|
|
16
|
+
isDeleted: { type: Boolean, default: false },
|
|
17
|
+
createdAt: { type: Number, required: true, default: Date.now() },
|
|
18
|
+
updatedAt: { type: Number, required: true },
|
|
19
|
+
deletedAt: { type: Number, required: false },
|
|
20
|
+
createdBy: {
|
|
21
|
+
type: Schema.Types.ObjectId,
|
|
22
|
+
ref: "User",
|
|
23
|
+
required: false,
|
|
24
|
+
},
|
|
25
|
+
updatedBy: {
|
|
26
|
+
type: Schema.Types.ObjectId,
|
|
27
|
+
ref: "User",
|
|
28
|
+
required: false,
|
|
29
|
+
},
|
|
30
|
+
deletedBy: {
|
|
31
|
+
type: Schema.Types.ObjectId,
|
|
32
|
+
ref: "User",
|
|
33
|
+
required: false,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function BaseSchemaPlugin(schema: Schema) {
|
|
38
|
+
schema.add({
|
|
39
|
+
isDeleted: { type: Boolean, default: false },
|
|
40
|
+
createdAt: { type: Number, default: () => Date.now() },
|
|
41
|
+
updatedAt: { type: Number, default: () => Date.now() },
|
|
42
|
+
deletedAt: { type: Number },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
schema.pre("save", function () {
|
|
46
|
+
this.updatedAt = Date.now();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
schema.pre("findOneAndUpdate", function () {
|
|
50
|
+
this.set({ updatedAt: Date.now() });
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Schema, model, Document, Model } from "mongoose";
|
|
2
|
+
import { ISpecialtyDocument, SpecialtyModel } from ".";
|
|
3
|
+
import { IUserDocument, UserModel } from ".";
|
|
4
|
+
import { BaseSchemaFields, BaseSchemaPlugin, IBaseModel } from ".";
|
|
5
|
+
import { EducationSchema, IEducation } from ".";
|
|
6
|
+
import { PositionSchema, IPosition } from ".";
|
|
7
|
+
import { LanguageSchema, ILanguage } from ".";
|
|
8
|
+
import { FaqSchema, IFaq } from ".";
|
|
9
|
+
import { LocationSchema, ILocation } from ".";
|
|
10
|
+
|
|
11
|
+
export interface IDoctor extends IBaseModel {
|
|
12
|
+
name: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
isActive: boolean;
|
|
15
|
+
user: IUserDocument;
|
|
16
|
+
specialty: ISpecialtyDocument;
|
|
17
|
+
biography: string;
|
|
18
|
+
consultationFee: number;
|
|
19
|
+
isVerified: boolean;
|
|
20
|
+
isVisible: boolean;
|
|
21
|
+
isDeactivatedByAdmin: boolean;
|
|
22
|
+
photo?: string;
|
|
23
|
+
educations: IEducation[];
|
|
24
|
+
positions: IPosition[];
|
|
25
|
+
languages: ILanguage[];
|
|
26
|
+
faqs: IFaq[];
|
|
27
|
+
expertises: string[];
|
|
28
|
+
location?: ILocation;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IDoctorDocument extends IDoctor, Document {}
|
|
32
|
+
|
|
33
|
+
export interface IDoctorModel extends Model<IDoctorDocument> {}
|
|
34
|
+
|
|
35
|
+
const DoctorSchema = new Schema<IDoctorDocument>({
|
|
36
|
+
...BaseSchemaFields,
|
|
37
|
+
user: {
|
|
38
|
+
type: Schema.Types.ObjectId,
|
|
39
|
+
ref: UserModel,
|
|
40
|
+
required: true,
|
|
41
|
+
onDelete: "cascade",
|
|
42
|
+
},
|
|
43
|
+
specialty: {
|
|
44
|
+
type: Schema.Types.ObjectId,
|
|
45
|
+
ref: SpecialtyModel,
|
|
46
|
+
required: true,
|
|
47
|
+
},
|
|
48
|
+
name: { type: String, required: true, trim: true },
|
|
49
|
+
biography: { type: String, required: false },
|
|
50
|
+
slug: { type: String, required: true, unique: true, trim: true },
|
|
51
|
+
isActive: { type: Boolean, default: false },
|
|
52
|
+
consultationFee: { type: Number, required: false },
|
|
53
|
+
isVerified: { type: Boolean, default: false },
|
|
54
|
+
isVisible: { type: Boolean, default: true },
|
|
55
|
+
isDeactivatedByAdmin: { type: Boolean, default: false },
|
|
56
|
+
photo: { type: String, required: false },
|
|
57
|
+
educations: { type: [EducationSchema], required: true, default: [] },
|
|
58
|
+
positions: { type: [PositionSchema], required: true, default: [] },
|
|
59
|
+
languages: { type: [LanguageSchema], required: true, default: [] },
|
|
60
|
+
faqs: { type: [FaqSchema], required: true, default: [] },
|
|
61
|
+
expertises: { type: [String], required: true, default: [] },
|
|
62
|
+
location: { type: LocationSchema, required: false },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const createSlug = (text: string): string =>
|
|
66
|
+
text
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.trim()
|
|
69
|
+
.replace(/[\s\W-]+/g, "-")
|
|
70
|
+
.replace(/^-+|-+$/g, "");
|
|
71
|
+
|
|
72
|
+
DoctorSchema.pre<IDoctorDocument>("validate", async function (next) {
|
|
73
|
+
// Only set slug if it hasn't been set before
|
|
74
|
+
if (this.slug) return next();
|
|
75
|
+
|
|
76
|
+
const baseSlug = createSlug(this.name);
|
|
77
|
+
let slug = baseSlug;
|
|
78
|
+
let counter = 0;
|
|
79
|
+
|
|
80
|
+
const Doctor = this.constructor as IDoctorModel;
|
|
81
|
+
|
|
82
|
+
while (await Doctor.exists({ slug })) {
|
|
83
|
+
counter += 1;
|
|
84
|
+
slug = `${baseSlug}-${counter}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.slug = slug;
|
|
88
|
+
next();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
DoctorSchema.plugin(BaseSchemaPlugin);
|
|
92
|
+
|
|
93
|
+
export const DoctorModel = model<IDoctorDocument, IDoctorModel>(
|
|
94
|
+
"Doctor",
|
|
95
|
+
DoctorSchema
|
|
96
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Schema } from "mongoose";
|
|
2
|
+
|
|
3
|
+
export interface IEducation {
|
|
4
|
+
year: number;
|
|
5
|
+
title: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const EducationSchema = new Schema<IEducation>(
|
|
9
|
+
{
|
|
10
|
+
year: { type: Number, required: true },
|
|
11
|
+
title: { type: String, required: true, trim: true },
|
|
12
|
+
},
|
|
13
|
+
{ _id: false }
|
|
14
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Schema } from "mongoose";
|
|
2
|
+
|
|
3
|
+
export interface IFaq {
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const FaqSchema = new Schema<IFaq>(
|
|
9
|
+
{
|
|
10
|
+
title: { type: String, required: true, trim: true },
|
|
11
|
+
description: { type: String, required: true, trim: true },
|
|
12
|
+
},
|
|
13
|
+
{ _id: false }
|
|
14
|
+
);
|