exp-node-server 1.1.7 → 2.0.0
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/README.md +82 -20
- package/dist/cli.js +525 -0
- package/package.json +29 -9
- package/templates/mongoose/.dockerignore +10 -0
- package/templates/mongoose/.env.example +56 -7
- package/templates/mongoose/.prettierignore +3 -0
- package/templates/mongoose/.prettierrc +7 -0
- package/templates/mongoose/Dockerfile +33 -0
- package/templates/mongoose/_gitignore +32 -0
- package/templates/mongoose/docker-compose.yml +21 -0
- package/templates/mongoose/eslint.config.js +35 -0
- package/templates/mongoose/package-lock.json +6377 -0
- package/templates/mongoose/package.json +49 -10
- package/templates/mongoose/readme.md +85 -0
- package/templates/mongoose/src/app/configs/index.ts +178 -26
- package/templates/mongoose/src/app/errors/zodError.ts +16 -16
- package/templates/mongoose/src/app/lib/db.ts +18 -0
- package/templates/mongoose/src/app/lib/logger.ts +12 -0
- package/templates/mongoose/src/app/middlewares/auth.ts +48 -44
- package/templates/mongoose/src/app/middlewares/global_error_handler.ts +59 -65
- package/templates/mongoose/src/app/middlewares/not_found_api.ts +7 -7
- package/templates/mongoose/src/app/middlewares/rate_limiter.ts +153 -0
- package/templates/mongoose/src/app/middlewares/request_validator.ts +11 -10
- package/templates/mongoose/src/app/middlewares/uploader.ts +17 -13
- package/templates/mongoose/src/app/modules/account/account.controller.ts +156 -0
- package/templates/mongoose/src/app/modules/account/account.interface.ts +65 -0
- package/templates/mongoose/src/app/modules/account/account.route.ts +101 -0
- package/templates/mongoose/src/app/modules/account/account.schema.ts +97 -0
- package/templates/mongoose/src/app/modules/account/account.service.ts +422 -0
- package/templates/mongoose/src/app/modules/account/account.swagger.ts +278 -0
- package/templates/mongoose/src/app/modules/account/account.validation.ts +63 -0
- package/templates/mongoose/src/app/modules/profile/profile.controller.ts +17 -0
- package/templates/mongoose/src/app/modules/profile/profile.interface.ts +7 -0
- package/templates/mongoose/src/app/modules/profile/profile.route.ts +28 -0
- package/templates/mongoose/src/app/modules/profile/profile.schema.ts +13 -0
- package/templates/mongoose/src/app/modules/profile/profile.service.ts +28 -0
- package/templates/mongoose/src/app/modules/profile/profile.swagger.ts +34 -0
- package/templates/mongoose/src/app/modules/profile/profile.validation.ts +8 -0
- package/templates/mongoose/src/app/passport-js/index.ts +87 -0
- package/templates/mongoose/src/app/passport-js/oauth-state.ts +150 -0
- package/templates/mongoose/src/app/passport-js/providers.ts +258 -0
- package/templates/mongoose/src/app/passport-js/social-account.ts +141 -0
- package/templates/mongoose/src/app/queues/agenda.ts +24 -0
- package/templates/mongoose/src/app/queues/email.queue.ts +53 -0
- package/templates/mongoose/src/app/queues/index.ts +13 -0
- package/templates/mongoose/src/app/templates/otpEmailTemplate.ts +185 -0
- package/templates/mongoose/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
- package/templates/mongoose/src/app/types/error.d.ts +7 -7
- package/templates/mongoose/src/app/types/express.d.ts +11 -0
- package/templates/mongoose/src/app/types/passport-oauth1.d.ts +51 -0
- package/templates/mongoose/src/app/utils/JWT.ts +38 -18
- package/templates/mongoose/src/app/utils/app_error.ts +10 -10
- package/templates/mongoose/src/app/utils/auth_cookie.ts +27 -0
- package/templates/mongoose/src/app/utils/catch_async.ts +8 -8
- package/templates/mongoose/src/app/utils/cloudinary.ts +26 -59
- package/templates/mongoose/src/app/utils/mail_sender.ts +26 -92
- package/templates/mongoose/src/app/utils/manage_response.ts +19 -19
- package/templates/mongoose/src/app/utils/otp.ts +20 -0
- package/templates/mongoose/src/app/utils/session.ts +55 -0
- package/templates/mongoose/src/app.ts +73 -42
- package/templates/mongoose/src/routes.ts +13 -13
- package/templates/mongoose/src/server.ts +38 -18
- package/templates/mongoose/src/swaggerOptions.ts +23 -23
- package/templates/mongoose/tests/app.test.ts +97 -0
- package/templates/mongoose/tests/global-setup.ts +15 -0
- package/templates/mongoose/tests/helpers/fixtures.ts +42 -0
- package/templates/mongoose/tests/routes/account.test.ts +614 -0
- package/templates/mongoose/tests/routes/profile.test.ts +95 -0
- package/templates/mongoose/tests/routes/social.test.ts +310 -0
- package/templates/mongoose/tests/setup.ts +25 -0
- package/templates/mongoose/tsconfig.json +16 -20
- package/templates/mongoose/vitest.config.ts +43 -0
- package/templates/prisma/.dockerignore +2 -0
- package/templates/prisma/.env.example +62 -6
- package/templates/prisma/.prettierignore +3 -0
- package/templates/prisma/.prettierrc +7 -0
- package/templates/prisma/.projectrc.json +1 -1
- package/templates/prisma/Dockerfile +21 -7
- package/templates/prisma/_gitignore +35 -0
- package/templates/prisma/docker-compose.yml +8 -7
- package/templates/prisma/eslint.config.js +35 -0
- package/templates/prisma/package-lock.json +7327 -0
- package/templates/prisma/package.json +47 -9
- package/templates/prisma/prisma/schema/account.schema.prisma +92 -10
- package/templates/prisma/prisma/schema/profile.schema.prisma +8 -3
- package/templates/prisma/prisma/schema/schema.prisma +1 -2
- package/templates/prisma/prisma.config.ts +5 -5
- package/templates/prisma/readme.md +81 -0
- package/templates/prisma/src/app/configs/index.ts +175 -26
- package/templates/prisma/src/app/errors/zodError.ts +16 -16
- package/templates/prisma/src/app/lib/logger.ts +12 -0
- package/templates/prisma/src/app/lib/prisma.ts +4 -6
- package/templates/prisma/src/app/middlewares/auth.ts +37 -16
- package/templates/prisma/src/app/middlewares/global_error_handler.ts +42 -35
- package/templates/prisma/src/app/middlewares/not_found_api.ts +7 -7
- package/templates/prisma/src/app/middlewares/rate_limiter.ts +124 -0
- package/templates/prisma/src/app/middlewares/request_validator.ts +4 -3
- package/templates/prisma/src/app/middlewares/uploader.ts +17 -13
- package/templates/prisma/src/app/modules/account/account.controller.ts +74 -34
- package/templates/prisma/src/app/modules/account/account.route.ts +68 -22
- package/templates/prisma/src/app/modules/account/account.service.ts +329 -309
- package/templates/prisma/src/app/modules/account/account.swagger.ts +167 -76
- package/templates/prisma/src/app/modules/account/account.validation.ts +41 -19
- package/templates/prisma/src/app/modules/profile/profile.controller.ts +4 -4
- package/templates/prisma/src/app/modules/profile/profile.route.ts +18 -12
- package/templates/prisma/src/app/modules/profile/profile.service.ts +11 -18
- package/templates/prisma/src/app/modules/profile/profile.swagger.ts +11 -11
- package/templates/prisma/src/app/modules/profile/profile.validation.ts +2 -2
- package/templates/prisma/src/app/passport-js/index.ts +87 -0
- package/templates/prisma/src/app/passport-js/oauth-state.ts +150 -0
- package/templates/prisma/src/app/passport-js/providers.ts +263 -0
- package/templates/prisma/src/app/passport-js/social-account.ts +147 -0
- package/templates/prisma/src/app/queues/boss.ts +8 -0
- package/templates/prisma/src/app/queues/email.queue.ts +56 -0
- package/templates/prisma/src/app/queues/index.ts +14 -0
- package/templates/prisma/src/app/queues/maintenance.queue.ts +30 -0
- package/templates/prisma/src/app/templates/otpEmailTemplate.ts +185 -0
- package/templates/prisma/src/app/templates/resetPasswordEmailTemplate.ts +157 -0
- package/templates/prisma/src/app/types/error.d.ts +7 -7
- package/templates/prisma/src/app/types/express.d.ts +11 -0
- package/templates/prisma/src/app/types/passport-oauth1.d.ts +51 -0
- package/templates/prisma/src/app/utils/JWT.ts +36 -17
- package/templates/prisma/src/app/utils/app_error.ts +10 -10
- package/templates/prisma/src/app/utils/auth_cookie.ts +27 -0
- package/templates/prisma/src/app/utils/catch_async.ts +8 -8
- package/templates/prisma/src/app/utils/cloudinary.ts +26 -59
- package/templates/prisma/src/app/utils/mail_sender.ts +26 -92
- package/templates/prisma/src/app/utils/manage_response.ts +19 -19
- package/templates/prisma/src/app/utils/otp.ts +20 -0
- package/templates/prisma/src/app/utils/session.ts +57 -0
- package/templates/prisma/src/app.ts +73 -42
- package/templates/prisma/src/routes.ts +12 -10
- package/templates/prisma/src/server.ts +37 -19
- package/templates/prisma/src/swaggerOptions.ts +22 -26
- package/templates/prisma/tests/app.test.ts +86 -0
- package/templates/prisma/tests/helpers/fixtures.ts +35 -0
- package/templates/prisma/tests/helpers/prisma.ts +32 -0
- package/templates/prisma/tests/routes/account.test.ts +627 -0
- package/templates/prisma/tests/routes/profile.test.ts +91 -0
- package/templates/prisma/tests/routes/social.test.ts +317 -0
- package/templates/prisma/tests/setup.ts +10 -0
- package/templates/prisma/tsconfig.json +16 -23
- package/templates/prisma/vitest.config.ts +38 -0
- package/bin/cli.js +0 -41
- package/src/createProject.js +0 -61
- package/src/generateModule.js +0 -93
- package/src/utils/getProjectType.js +0 -13
- package/src/utils/mongoose/controllerGenerator.js +0 -70
- package/src/utils/mongoose/routeGenerator.js +0 -27
- package/src/utils/mongoose/schemaGenerator.js +0 -11
- package/src/utils/mongoose/serviceGenerator.js +0 -49
- package/src/utils/mongoose/swaggerGenerator.js +0 -112
- package/src/utils/mongoose/validationGenerator.js +0 -13
- package/src/utils/prisma/prismaSchemaGenerator.js +0 -10
- package/src/utils/prisma/prismaServiceGenerator.js +0 -47
- package/src/utils/routeInjector.js +0 -40
- package/src/utils/swaggerInjector.js +0 -52
- package/src/utils/validateModuleName.js +0 -17
- package/src/utils/validateProjectName.js +0 -15
- package/src/utils.js +0 -26
- package/templates/mongoose/src/app/errors/castError.ts +0 -23
- package/templates/mongoose/src/app/errors/duplicateError.ts +0 -26
- package/templates/mongoose/src/app/errors/validationError.ts +0 -25
- package/templates/mongoose/src/app/modules/auth/auth.controller.ts +0 -127
- package/templates/mongoose/src/app/modules/auth/auth.interface.ts +0 -24
- package/templates/mongoose/src/app/modules/auth/auth.route.ts +0 -46
- package/templates/mongoose/src/app/modules/auth/auth.schema.ts +0 -19
- package/templates/mongoose/src/app/modules/auth/auth.service.ts +0 -324
- package/templates/mongoose/src/app/modules/auth/auth.swagger.ts +0 -214
- package/templates/mongoose/src/app/modules/auth/auth.validation.ts +0 -37
- package/templates/mongoose/src/app/modules/user/user.controller.ts +0 -17
- package/templates/mongoose/src/app/modules/user/user.interface.ts +0 -15
- package/templates/mongoose/src/app/modules/user/user.route.ts +0 -20
- package/templates/mongoose/src/app/modules/user/user.schema.ts +0 -22
- package/templates/mongoose/src/app/modules/user/user.service.ts +0 -22
- package/templates/mongoose/src/app/modules/user/user.swagger.ts +0 -75
- package/templates/mongoose/src/app/modules/user/user.validation.ts +0 -18
- package/templates/mongoose/src/app/types/index.d.ts +0 -12
- package/templates/mongoose/src/app/utils/isAccountExist.ts +0 -31
- package/templates/prisma/src/app/types/index.d.ts +0 -12
- package/templates/prisma/src/app/utils/otpGenerator.ts +0 -29
|
@@ -1,50 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
1
|
+
import { Prisma } from '@prisma/client';
|
|
2
|
+
import type { ErrorRequestHandler } from 'express';
|
|
3
|
+
import multer from 'multer';
|
|
4
|
+
import { ZodError } from 'zod';
|
|
5
|
+
import { configs } from '../configs/index.js';
|
|
6
|
+
import handleZodError from '../errors/zodError.js';
|
|
7
|
+
import { logger } from '../lib/logger.js';
|
|
8
|
+
import type { TErrorSources } from '../types/error.js';
|
|
9
|
+
import { AppError } from '../utils/app_error.js';
|
|
7
10
|
|
|
8
|
-
const globalErrorHandler: ErrorRequestHandler = (err, req, res,
|
|
11
|
+
const globalErrorHandler: ErrorRequestHandler = (err, req, res, _next) => {
|
|
9
12
|
let statusCode = 500;
|
|
10
|
-
let message =
|
|
11
|
-
let errorSources: TErrorSources = [
|
|
12
|
-
{
|
|
13
|
-
path: "",
|
|
14
|
-
message: "Something went wrong",
|
|
15
|
-
},
|
|
16
|
-
];
|
|
13
|
+
let message = 'Something went wrong!';
|
|
14
|
+
let errorSources: TErrorSources = [];
|
|
17
15
|
|
|
18
16
|
if (err instanceof ZodError) {
|
|
19
|
-
|
|
20
|
-
statusCode = simplifiedError?.statusCode;
|
|
21
|
-
message = simplifiedError?.message;
|
|
22
|
-
errorSources = simplifiedError?.errorSources;
|
|
17
|
+
({ statusCode, message, errorSources } = handleZodError(err));
|
|
23
18
|
} else if (err instanceof AppError) {
|
|
24
|
-
statusCode = err
|
|
19
|
+
statusCode = err.statusCode;
|
|
25
20
|
message = err.message;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
path: "",
|
|
29
|
-
message: err?.message,
|
|
30
|
-
},
|
|
31
|
-
];
|
|
32
|
-
} else if (err instanceof Error) {
|
|
21
|
+
} else if (err instanceof multer.MulterError) {
|
|
22
|
+
statusCode = 400;
|
|
33
23
|
message = err.message;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
} else if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') {
|
|
25
|
+
statusCode = 409;
|
|
26
|
+
message = 'Resource already exists';
|
|
27
|
+
} else if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2025') {
|
|
28
|
+
statusCode = 404;
|
|
29
|
+
message = 'Resource not found';
|
|
30
|
+
} else if (err?.status === 401 || err?.name === 'AuthenticationError') {
|
|
31
|
+
// passport authentication failures
|
|
32
|
+
statusCode = 401;
|
|
33
|
+
message = 'Authentication failed';
|
|
34
|
+
} else if (err instanceof SyntaxError && 'body' in err) {
|
|
35
|
+
// malformed JSON body
|
|
36
|
+
statusCode = 400;
|
|
37
|
+
message = 'Invalid JSON body';
|
|
38
|
+
} else if (err?.expose && typeof err.status === 'number' && err.status < 500) {
|
|
39
|
+
// client errors from body-parser & co (body too large, unsupported charset, ...)
|
|
40
|
+
statusCode = err.status;
|
|
41
|
+
message = err.message;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (statusCode >= 500) {
|
|
45
|
+
logger.error({ err, method: req.method, url: req.originalUrl }, 'Unhandled error');
|
|
46
|
+
// don't leak internals (SQL, file paths, ...) to clients in production
|
|
47
|
+
if (!configs.is_production && err instanceof Error) message = err.message;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
res.status(statusCode).json({
|
|
43
51
|
success: false,
|
|
44
52
|
message,
|
|
45
|
-
errorSources,
|
|
46
|
-
err,
|
|
47
|
-
stack: configs.env === "development" ? err?.stack : null,
|
|
53
|
+
errorSources: errorSources.length ? errorSources : [{ path: '', message }],
|
|
54
|
+
...(configs.is_production ? {} : { stack: err?.stack }),
|
|
48
55
|
});
|
|
49
56
|
};
|
|
50
57
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from 'express';
|
|
2
2
|
|
|
3
|
-
const notFound = (req: Request, res: Response,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const notFound = (req: Request, res: Response, _next: NextFunction) => {
|
|
4
|
+
res.status(404).json({
|
|
5
|
+
message: 'Sorry Route is not found!! 😴😴😴',
|
|
6
|
+
success: false,
|
|
7
|
+
error: '',
|
|
8
|
+
});
|
|
9
9
|
};
|
|
10
|
-
export default notFound;
|
|
10
|
+
export default notFound;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
import rateLimit, {
|
|
3
|
+
type IncrementResponse,
|
|
4
|
+
ipKeyGenerator,
|
|
5
|
+
type Options,
|
|
6
|
+
type Store,
|
|
7
|
+
} from 'express-rate-limit';
|
|
8
|
+
import { configs } from '../configs/index.js';
|
|
9
|
+
import { prisma } from '../lib/prisma.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Keeps hit counters in the `RateLimit` table, so limits are shared by every app
|
|
13
|
+
* instance and survive restarts (no Redis needed). Expired rows are removed by the
|
|
14
|
+
* maintenance job (see queues/maintenance.queue.ts).
|
|
15
|
+
*/
|
|
16
|
+
class PostgresStore implements Store {
|
|
17
|
+
windowMs = 60_000;
|
|
18
|
+
// `prefix` keeps counters of different limiters apart in the same table
|
|
19
|
+
constructor(public prefix: string) {}
|
|
20
|
+
|
|
21
|
+
init(options: Options) {
|
|
22
|
+
this.windowMs = options.windowMs;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async increment(key: string): Promise<IncrementResponse> {
|
|
26
|
+
// one atomic upsert: start a new window if the old one is over, else count the hit
|
|
27
|
+
const [row] = await prisma.$queryRaw<{ hits: number; resetAt: Date }[]>`
|
|
28
|
+
INSERT INTO "RateLimit" ("key", "hits", "resetAt")
|
|
29
|
+
VALUES (${this.prefix + key}, 1, now() + ${this.windowMs}::int * interval '1 millisecond')
|
|
30
|
+
ON CONFLICT ("key") DO UPDATE SET
|
|
31
|
+
"hits" = CASE WHEN "RateLimit"."resetAt" <= now() THEN 1 ELSE "RateLimit"."hits" + 1 END,
|
|
32
|
+
"resetAt" = CASE WHEN "RateLimit"."resetAt" <= now() THEN EXCLUDED."resetAt" ELSE "RateLimit"."resetAt" END
|
|
33
|
+
RETURNING "hits", "resetAt"`;
|
|
34
|
+
return { totalHits: row!.hits, resetTime: row!.resetAt };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async decrement(key: string) {
|
|
38
|
+
await prisma.rateLimit.updateMany({
|
|
39
|
+
where: { key: this.prefix + key, hits: { gt: 0 } },
|
|
40
|
+
data: { hits: { decrement: 1 } },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async resetKey(key: string) {
|
|
45
|
+
await prisma.rateLimit.deleteMany({ where: { key: this.prefix + key } });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const message = { success: false, message: 'Too many attempts, please try again later.' };
|
|
50
|
+
|
|
51
|
+
/** Client IP, with IPv6 addresses grouped by subnet so they can't rotate around the limit. */
|
|
52
|
+
const ip = (req: Request) => ipKeyGenerator(req.ip ?? '');
|
|
53
|
+
|
|
54
|
+
/** Email from the (not yet validated) body, normalised like the validators do. */
|
|
55
|
+
const email = (req: Request) =>
|
|
56
|
+
typeof req.body?.email === 'string' ? req.body.email.trim().toLowerCase() : '';
|
|
57
|
+
|
|
58
|
+
type LimiterOptions = {
|
|
59
|
+
windowMs: number;
|
|
60
|
+
limit: number;
|
|
61
|
+
keyGenerator?: (req: Request) => string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** A limiter backed by Postgres (in-memory under tests). `name` must be unique. */
|
|
65
|
+
const createLimiter = (name: string, { windowMs, limit, keyGenerator = ip }: LimiterOptions) =>
|
|
66
|
+
rateLimit({
|
|
67
|
+
windowMs,
|
|
68
|
+
limit,
|
|
69
|
+
keyGenerator,
|
|
70
|
+
standardHeaders: 'draft-8',
|
|
71
|
+
legacyHeaders: false,
|
|
72
|
+
message,
|
|
73
|
+
store: configs.is_test ? undefined : new PostgresStore(`${name}:`),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const MINUTE = 60 * 1000;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Coarse limit for the whole API. Kept in memory (per instance) so normal traffic
|
|
80
|
+
* doesn't pay for a database round trip; the auth limiters below are the strict ones.
|
|
81
|
+
*/
|
|
82
|
+
export const apiRateLimiter = rateLimit({
|
|
83
|
+
windowMs: MINUTE,
|
|
84
|
+
limit: 300,
|
|
85
|
+
standardHeaders: 'draft-8',
|
|
86
|
+
legacyHeaders: false,
|
|
87
|
+
message,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const rateLimiters = {
|
|
91
|
+
/** Account creation, per IP. */
|
|
92
|
+
signUp: createLimiter('sign-up', { windowMs: 60 * MINUTE, limit: 10 }),
|
|
93
|
+
|
|
94
|
+
/** Password guessing: per IP+email for one account, and per IP across many accounts. */
|
|
95
|
+
signIn: createLimiter('sign-in', {
|
|
96
|
+
windowMs: 15 * MINUTE,
|
|
97
|
+
limit: 10,
|
|
98
|
+
keyGenerator: (req) => `${ip(req)}:${email(req)}`,
|
|
99
|
+
}),
|
|
100
|
+
signInIp: createLimiter('sign-in-ip', { windowMs: 15 * MINUTE, limit: 50 }),
|
|
101
|
+
|
|
102
|
+
/** OTP / link / reset token guessing, per IP. */
|
|
103
|
+
verify: createLimiter('verify', { windowMs: 15 * MINUTE, limit: 20 }),
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Endpoints that send an email. Limited per recipient so nobody can flood an
|
|
107
|
+
* inbox from many IPs, and per IP so one client can't spray many addresses.
|
|
108
|
+
*/
|
|
109
|
+
emailPerAddress: createLimiter('email-address', {
|
|
110
|
+
windowMs: 15 * MINUTE,
|
|
111
|
+
limit: 3,
|
|
112
|
+
keyGenerator: email,
|
|
113
|
+
}),
|
|
114
|
+
emailPerIp: createLimiter('email-ip', { windowMs: 15 * MINUTE, limit: 10 }),
|
|
115
|
+
|
|
116
|
+
/** Password changes for signed-in users, per IP. */
|
|
117
|
+
changePassword: createLimiter('change-password', { windowMs: 15 * MINUTE, limit: 10 }),
|
|
118
|
+
|
|
119
|
+
/** Token refreshes, per IP (tokens are unguessable; this only caps abuse). */
|
|
120
|
+
refresh: createLimiter('refresh', { windowMs: 15 * MINUTE, limit: 300 }),
|
|
121
|
+
|
|
122
|
+
/** Starting a social login and exchanging its code, per IP. */
|
|
123
|
+
social: createLimiter('social', { windowMs: 15 * MINUTE, limit: 30 }),
|
|
124
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { NextFunction, Request, Response } from
|
|
1
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
2
|
+
import type { ZodType } from 'zod';
|
|
2
3
|
|
|
3
|
-
const RequestValidator = (schema:
|
|
4
|
-
return async (req: Request,
|
|
4
|
+
const RequestValidator = (schema: ZodType) => {
|
|
5
|
+
return async (req: Request, _res: Response, next: NextFunction) => {
|
|
5
6
|
try {
|
|
6
7
|
req.body = await schema.parseAsync(req.body);
|
|
7
8
|
next();
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import multer from
|
|
2
|
-
import
|
|
1
|
+
import multer from 'multer';
|
|
2
|
+
import { AppError } from '../utils/app_error.js';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
cb(null, path.join(process.cwd(), "uploads"))
|
|
7
|
-
},
|
|
8
|
-
filename: function (req, file, cb) {
|
|
4
|
+
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
|
5
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Keeps the (size limited) file in memory and streams it to Cloudinary, so nothing
|
|
9
|
+
* is ever left behind on disk when validation or the upload fails.
|
|
10
|
+
*/
|
|
11
|
+
const uploader = multer({
|
|
12
|
+
storage: multer.memoryStorage(),
|
|
13
|
+
limits: { fileSize: MAX_FILE_SIZE, files: 1, fields: 10 },
|
|
14
|
+
fileFilter: (_req, file, cb) => {
|
|
15
|
+
if (ALLOWED_IMAGE_TYPES.includes(file.mimetype)) return cb(null, true);
|
|
16
|
+
cb(new AppError(`Only ${ALLOWED_IMAGE_TYPES.join(', ')} files are allowed`, 400));
|
|
17
|
+
},
|
|
18
|
+
});
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export default uploader;
|
|
20
|
+
export default uploader;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import { account_services } from
|
|
1
|
+
import catchAsync from '../../utils/catch_async.js';
|
|
2
|
+
import manageResponse from '../../utils/manage_response.js';
|
|
3
|
+
import { clearSessionCookies, createSession } from '../../utils/session.js';
|
|
4
|
+
import { account_services } from './account.service.js';
|
|
5
5
|
|
|
6
6
|
const create_account = catchAsync(async (req, res) => {
|
|
7
7
|
const result = await account_services.create_account_into_db(req);
|
|
8
8
|
manageResponse(res, {
|
|
9
|
-
statusCode:
|
|
9
|
+
statusCode: 201,
|
|
10
10
|
success: true,
|
|
11
|
-
message:
|
|
11
|
+
message: 'Account created successfully',
|
|
12
12
|
data: result,
|
|
13
13
|
});
|
|
14
14
|
});
|
|
@@ -18,7 +18,7 @@ const verify_account_using_otp = catchAsync(async (req, res) => {
|
|
|
18
18
|
manageResponse(res, {
|
|
19
19
|
statusCode: 200,
|
|
20
20
|
success: true,
|
|
21
|
-
message:
|
|
21
|
+
message: 'Otp verification successful',
|
|
22
22
|
data: result,
|
|
23
23
|
});
|
|
24
24
|
});
|
|
@@ -28,68 +28,61 @@ const verify_account_using_link = catchAsync(async (req, res) => {
|
|
|
28
28
|
manageResponse(res, {
|
|
29
29
|
statusCode: 200,
|
|
30
30
|
success: true,
|
|
31
|
-
message:
|
|
31
|
+
message: 'Account verification successful',
|
|
32
32
|
data: result,
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
const login_user = catchAsync(async (req, res) => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
// set access token into cookie
|
|
40
|
-
res.cookie("access_token", result, {
|
|
41
|
-
secure: configs.env === "production",
|
|
42
|
-
httpOnly: true,
|
|
43
|
-
});
|
|
44
|
-
|
|
37
|
+
const user = await account_services.login_user_into_db(req);
|
|
38
|
+
const tokens = await createSession(res, user);
|
|
45
39
|
manageResponse(res, {
|
|
46
40
|
statusCode: 200,
|
|
47
41
|
success: true,
|
|
48
|
-
message:
|
|
49
|
-
data:
|
|
50
|
-
accessToken: result,
|
|
51
|
-
},
|
|
42
|
+
message: 'User logged in successfully',
|
|
43
|
+
data: tokens,
|
|
52
44
|
});
|
|
53
45
|
});
|
|
46
|
+
|
|
54
47
|
const get_user_account = catchAsync(async (req, res) => {
|
|
55
48
|
const result = await account_services.get_user_account_from_db(req);
|
|
56
49
|
|
|
57
50
|
manageResponse(res, {
|
|
58
51
|
statusCode: 200,
|
|
59
52
|
success: true,
|
|
60
|
-
message:
|
|
53
|
+
message: 'Account fetched successfully',
|
|
61
54
|
data: result,
|
|
62
55
|
});
|
|
63
56
|
});
|
|
64
57
|
|
|
65
58
|
const change_password = catchAsync(async (req, res) => {
|
|
66
|
-
const
|
|
59
|
+
const user = await account_services.change_password_into_db(req);
|
|
60
|
+
// every session was revoked, so give this one a fresh start
|
|
61
|
+
const tokens = await createSession(res, user);
|
|
67
62
|
manageResponse(res, {
|
|
68
63
|
statusCode: 200,
|
|
69
64
|
success: true,
|
|
70
|
-
message:
|
|
71
|
-
data:
|
|
65
|
+
message: 'Password changed successfully',
|
|
66
|
+
data: tokens,
|
|
72
67
|
});
|
|
73
68
|
});
|
|
74
69
|
|
|
75
70
|
const resend_otp_and_verification_link = catchAsync(async (req, res) => {
|
|
76
|
-
const result =
|
|
77
|
-
await account_services.resend_otp_and_verification_link_from_db(req);
|
|
71
|
+
const result = await account_services.resend_otp_and_verification_link_from_db(req);
|
|
78
72
|
manageResponse(res, {
|
|
79
73
|
statusCode: 200,
|
|
80
74
|
success: true,
|
|
81
|
-
message:
|
|
75
|
+
message: 'If the account exists and is not verified, a new OTP has been sent',
|
|
82
76
|
data: result,
|
|
83
77
|
});
|
|
84
78
|
});
|
|
85
79
|
|
|
86
|
-
const
|
|
87
|
-
const result =
|
|
88
|
-
await account_services.forget_password_genereate_reset_token_from_db(req);
|
|
80
|
+
const forget_password_generate_reset_token = catchAsync(async (req, res) => {
|
|
81
|
+
const result = await account_services.forget_password_generate_reset_token_from_db(req);
|
|
89
82
|
manageResponse(res, {
|
|
90
83
|
statusCode: 200,
|
|
91
84
|
success: true,
|
|
92
|
-
message:
|
|
85
|
+
message: 'If an account exists for this email, a password reset link has been sent',
|
|
93
86
|
data: result,
|
|
94
87
|
});
|
|
95
88
|
});
|
|
@@ -99,10 +92,53 @@ const reset_password_using_token = catchAsync(async (req, res) => {
|
|
|
99
92
|
manageResponse(res, {
|
|
100
93
|
statusCode: 200,
|
|
101
94
|
success: true,
|
|
102
|
-
message:
|
|
95
|
+
message: 'Password reset successfully',
|
|
103
96
|
data: result,
|
|
104
97
|
});
|
|
105
98
|
});
|
|
99
|
+
|
|
100
|
+
const refresh_token = catchAsync(async (req, res) => {
|
|
101
|
+
const { user, familyId } = await account_services.refresh_session_from_db(req);
|
|
102
|
+
const tokens = await createSession(res, user, familyId);
|
|
103
|
+
manageResponse(res, {
|
|
104
|
+
statusCode: 200,
|
|
105
|
+
success: true,
|
|
106
|
+
message: 'Tokens refreshed successfully',
|
|
107
|
+
data: tokens,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const sign_out = catchAsync(async (req, res) => {
|
|
112
|
+
await account_services.sign_out_from_db(req);
|
|
113
|
+
clearSessionCookies(res);
|
|
114
|
+
manageResponse(res, {
|
|
115
|
+
statusCode: 200,
|
|
116
|
+
success: true,
|
|
117
|
+
message: 'Signed out successfully',
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const sign_out_everywhere = catchAsync(async (req, res) => {
|
|
122
|
+
await account_services.sign_out_everywhere_from_db(req);
|
|
123
|
+
clearSessionCookies(res);
|
|
124
|
+
manageResponse(res, {
|
|
125
|
+
statusCode: 200,
|
|
126
|
+
success: true,
|
|
127
|
+
message: 'Signed out from all devices',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const exchange_social_login_code = catchAsync(async (req, res) => {
|
|
132
|
+
const user = await account_services.exchange_social_login_code_from_db(req);
|
|
133
|
+
const tokens = await createSession(res, user);
|
|
134
|
+
manageResponse(res, {
|
|
135
|
+
statusCode: 200,
|
|
136
|
+
success: true,
|
|
137
|
+
message: 'User logged in successfully',
|
|
138
|
+
data: tokens,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
106
142
|
export const account_controller = {
|
|
107
143
|
create_account,
|
|
108
144
|
login_user,
|
|
@@ -111,6 +147,10 @@ export const account_controller = {
|
|
|
111
147
|
verify_account_using_otp,
|
|
112
148
|
resend_otp_and_verification_link,
|
|
113
149
|
verify_account_using_link,
|
|
114
|
-
|
|
115
|
-
reset_password_using_token
|
|
150
|
+
forget_password_generate_reset_token,
|
|
151
|
+
reset_password_using_token,
|
|
152
|
+
refresh_token,
|
|
153
|
+
sign_out,
|
|
154
|
+
sign_out_everywhere,
|
|
155
|
+
exchange_social_login_code,
|
|
116
156
|
};
|
|
@@ -1,55 +1,101 @@
|
|
|
1
|
-
import { Router } from
|
|
2
|
-
import auth from
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import auth from '../../middlewares/auth.js';
|
|
3
|
+
import { rateLimiters } from '../../middlewares/rate_limiter.js';
|
|
4
|
+
import RequestValidator from '../../middlewares/request_validator.js';
|
|
5
|
+
import { socialAuthRouter } from '../../passport-js/index.js';
|
|
6
|
+
import { account_controller } from './account.controller.js';
|
|
7
|
+
import { account_validation } from './account.validation.js';
|
|
6
8
|
|
|
7
9
|
const accountRouter = Router();
|
|
8
10
|
|
|
9
11
|
accountRouter.post(
|
|
10
|
-
|
|
12
|
+
'/sign-up',
|
|
13
|
+
rateLimiters.signUp,
|
|
14
|
+
rateLimiters.emailPerAddress,
|
|
11
15
|
RequestValidator(account_validation.sign_up),
|
|
12
16
|
account_controller.create_account,
|
|
13
17
|
);
|
|
18
|
+
|
|
14
19
|
accountRouter.post(
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
'/sign-in',
|
|
21
|
+
rateLimiters.signInIp,
|
|
22
|
+
rateLimiters.signIn,
|
|
23
|
+
RequestValidator(account_validation.sign_in),
|
|
17
24
|
account_controller.login_user,
|
|
18
25
|
);
|
|
26
|
+
|
|
27
|
+
accountRouter.post(
|
|
28
|
+
'/refresh-token',
|
|
29
|
+
rateLimiters.refresh,
|
|
30
|
+
RequestValidator(account_validation.refresh_token),
|
|
31
|
+
account_controller.refresh_token,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
accountRouter.post(
|
|
35
|
+
'/sign-out',
|
|
36
|
+
RequestValidator(account_validation.refresh_token),
|
|
37
|
+
account_controller.sign_out,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
accountRouter.post('/sign-out-all', auth(), account_controller.sign_out_everywhere);
|
|
41
|
+
|
|
19
42
|
accountRouter.put(
|
|
20
|
-
|
|
43
|
+
'/verify-otp',
|
|
44
|
+
rateLimiters.verify,
|
|
21
45
|
RequestValidator(account_validation.verify_otp),
|
|
22
46
|
account_controller.verify_account_using_otp,
|
|
23
47
|
);
|
|
48
|
+
|
|
24
49
|
accountRouter.put(
|
|
25
|
-
|
|
50
|
+
'/verify-link',
|
|
51
|
+
rateLimiters.verify,
|
|
26
52
|
RequestValidator(account_validation.verify_link),
|
|
27
53
|
account_controller.verify_account_using_link,
|
|
28
54
|
);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
account_controller.get_user_account,
|
|
33
|
-
);
|
|
55
|
+
|
|
56
|
+
accountRouter.get('/me', auth(), account_controller.get_user_account);
|
|
57
|
+
|
|
34
58
|
accountRouter.put(
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
'/change-password',
|
|
60
|
+
rateLimiters.changePassword,
|
|
61
|
+
auth(),
|
|
37
62
|
RequestValidator(account_validation.change_password),
|
|
38
63
|
account_controller.change_password,
|
|
39
64
|
);
|
|
65
|
+
|
|
40
66
|
accountRouter.put(
|
|
41
|
-
|
|
67
|
+
'/resend-otp',
|
|
68
|
+
rateLimiters.emailPerIp,
|
|
69
|
+
rateLimiters.emailPerAddress,
|
|
42
70
|
RequestValidator(account_validation.resend_otp),
|
|
43
71
|
account_controller.resend_otp_and_verification_link,
|
|
44
72
|
);
|
|
73
|
+
|
|
45
74
|
accountRouter.put(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
'/forget-password',
|
|
76
|
+
rateLimiters.emailPerIp,
|
|
77
|
+
rateLimiters.emailPerAddress,
|
|
78
|
+
RequestValidator(account_validation.forget_password),
|
|
79
|
+
account_controller.forget_password_generate_reset_token,
|
|
49
80
|
);
|
|
81
|
+
|
|
50
82
|
accountRouter.put(
|
|
51
|
-
|
|
83
|
+
'/reset-password',
|
|
84
|
+
rateLimiters.verify,
|
|
52
85
|
RequestValidator(account_validation.reset_pass),
|
|
53
86
|
account_controller.reset_password_using_token,
|
|
54
87
|
);
|
|
88
|
+
|
|
89
|
+
// Social login: the front-end trades the `?code=` from the redirect for a token
|
|
90
|
+
accountRouter.post(
|
|
91
|
+
'/social/exchange',
|
|
92
|
+
rateLimiters.social,
|
|
93
|
+
RequestValidator(account_validation.social_exchange),
|
|
94
|
+
account_controller.exchange_social_login_code,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// GET /social/providers, /social/<provider> and /social/<provider>/callback
|
|
98
|
+
// for every provider enabled in .env (see passport-js/providers.ts)
|
|
99
|
+
accountRouter.use('/social', socialAuthRouter);
|
|
100
|
+
|
|
55
101
|
export default accountRouter;
|