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,69 +1,63 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import type { ErrorRequestHandler } from 'express';
|
|
2
|
+
import mongoose from 'mongoose';
|
|
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';
|
|
10
10
|
|
|
11
|
-
const globalErrorHandler: ErrorRequestHandler = (err, req, res,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
path: '',
|
|
17
|
-
message: 'Something went wrong'
|
|
18
|
-
}
|
|
19
|
-
]
|
|
11
|
+
const globalErrorHandler: ErrorRequestHandler = (err, req, res, _next) => {
|
|
12
|
+
let statusCode = 500;
|
|
13
|
+
let message = 'Something went wrong!';
|
|
14
|
+
let errorSources: TErrorSources = [];
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
path: '',
|
|
55
|
-
message: err?.message
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
16
|
+
if (err instanceof ZodError) {
|
|
17
|
+
({ statusCode, message, errorSources } = handleZodError(err));
|
|
18
|
+
} else if (err instanceof AppError) {
|
|
19
|
+
statusCode = err.statusCode;
|
|
20
|
+
message = err.message;
|
|
21
|
+
} else if (err instanceof multer.MulterError) {
|
|
22
|
+
statusCode = 400;
|
|
23
|
+
message = err.message;
|
|
24
|
+
} else if (err?.code === 11000) {
|
|
25
|
+
// duplicate key; the offending value is not echoed back
|
|
26
|
+
statusCode = 409;
|
|
27
|
+
message = 'Resource already exists';
|
|
28
|
+
} else if (err instanceof mongoose.Error.CastError) {
|
|
29
|
+
statusCode = 400;
|
|
30
|
+
message = `Invalid ${err.path}`;
|
|
31
|
+
} else if (err instanceof mongoose.Error.ValidationError) {
|
|
32
|
+
statusCode = 400;
|
|
33
|
+
message = 'Validation Error';
|
|
34
|
+
errorSources = Object.values(err.errors).map((e) => ({ path: e.path, message: e.message }));
|
|
35
|
+
} else if (err?.status === 401 || err?.name === 'AuthenticationError') {
|
|
36
|
+
// passport authentication failures
|
|
37
|
+
statusCode = 401;
|
|
38
|
+
message = 'Authentication failed';
|
|
39
|
+
} else if (err instanceof SyntaxError && 'body' in err) {
|
|
40
|
+
// malformed JSON body
|
|
41
|
+
statusCode = 400;
|
|
42
|
+
message = 'Invalid JSON body';
|
|
43
|
+
} else if (err?.expose && typeof err.status === 'number' && err.status < 500) {
|
|
44
|
+
// client errors from body-parser & co (body too large, unsupported charset, ...)
|
|
45
|
+
statusCode = err.status;
|
|
46
|
+
message = err.message;
|
|
47
|
+
}
|
|
59
48
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
stack: configs.env === 'development' ? err?.stack : null
|
|
66
|
-
})
|
|
67
|
-
}
|
|
49
|
+
if (statusCode >= 500) {
|
|
50
|
+
logger.error({ err, method: req.method, url: req.originalUrl }, 'Unhandled error');
|
|
51
|
+
// don't leak internals (SQL, file paths, ...) to clients in production
|
|
52
|
+
if (!configs.is_production && err instanceof Error) message = err.message;
|
|
53
|
+
}
|
|
68
54
|
|
|
69
|
-
|
|
55
|
+
res.status(statusCode).json({
|
|
56
|
+
success: false,
|
|
57
|
+
message,
|
|
58
|
+
errorSources: errorSources.length ? errorSources : [{ path: '', message }],
|
|
59
|
+
...(configs.is_production ? {} : { stack: err?.stack }),
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default globalErrorHandler;
|
|
@@ -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,153 @@
|
|
|
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 type { Collection } from 'mongodb';
|
|
9
|
+
import { model, Schema } from 'mongoose';
|
|
10
|
+
|
|
11
|
+
type RateLimitDoc = { _id: string; hits: number; resetAt: Date };
|
|
12
|
+
|
|
13
|
+
const rateLimitModel = model(
|
|
14
|
+
'rate_limit',
|
|
15
|
+
new Schema<RateLimitDoc>(
|
|
16
|
+
{
|
|
17
|
+
_id: { type: String, required: true },
|
|
18
|
+
hits: { type: Number, required: true },
|
|
19
|
+
// TTL index: MongoDB deletes counters once their window is over
|
|
20
|
+
resetAt: { type: Date, required: true, expires: 0 },
|
|
21
|
+
},
|
|
22
|
+
{ versionKey: false },
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const isDuplicateKey = (err: unknown) => (err as { code?: number })?.code === 11000;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Keeps hit counters in MongoDB, so limits are shared by every app instance and
|
|
30
|
+
* survive restarts (no Redis needed).
|
|
31
|
+
*/
|
|
32
|
+
class MongoStore implements Store {
|
|
33
|
+
windowMs = 60_000;
|
|
34
|
+
// `prefix` keeps counters of different limiters apart in the same collection
|
|
35
|
+
constructor(public prefix: string) {}
|
|
36
|
+
|
|
37
|
+
init(options: Options) {
|
|
38
|
+
this.windowMs = options.windowMs;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async increment(key: string): Promise<IncrementResponse> {
|
|
42
|
+
// one atomic update: start a new window if the old one is over, else count the hit
|
|
43
|
+
const update = [
|
|
44
|
+
{
|
|
45
|
+
$set: {
|
|
46
|
+
hits: { $cond: [{ $gt: ['$resetAt', '$$NOW'] }, { $add: ['$hits', 1] }, 1] },
|
|
47
|
+
resetAt: {
|
|
48
|
+
$cond: [{ $gt: ['$resetAt', '$$NOW'] }, '$resetAt', { $add: ['$$NOW', this.windowMs] }],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
const collection = rateLimitModel.collection as unknown as Collection<RateLimitDoc>;
|
|
54
|
+
const run = () =>
|
|
55
|
+
collection.findOneAndUpdate({ _id: this.prefix + key }, update, {
|
|
56
|
+
upsert: true,
|
|
57
|
+
returnDocument: 'after',
|
|
58
|
+
includeResultMetadata: false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// two first hits at the same moment can race on the upsert; the loser retries
|
|
62
|
+
const row = await run().catch((err) => (isDuplicateKey(err) ? run() : Promise.reject(err)));
|
|
63
|
+
return { totalHits: row!.hits, resetTime: row!.resetAt };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async decrement(key: string) {
|
|
67
|
+
await rateLimitModel.updateOne(
|
|
68
|
+
{ _id: this.prefix + key, hits: { $gt: 0 } },
|
|
69
|
+
{ $inc: { hits: -1 } },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async resetKey(key: string) {
|
|
74
|
+
await rateLimitModel.deleteOne({ _id: this.prefix + key });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const message = { success: false, message: 'Too many attempts, please try again later.' };
|
|
79
|
+
|
|
80
|
+
/** Client IP, with IPv6 addresses grouped by subnet so they can't rotate around the limit. */
|
|
81
|
+
const ip = (req: Request) => ipKeyGenerator(req.ip ?? '');
|
|
82
|
+
|
|
83
|
+
/** Email from the (not yet validated) body, normalised like the validators do. */
|
|
84
|
+
const email = (req: Request) =>
|
|
85
|
+
typeof req.body?.email === 'string' ? req.body.email.trim().toLowerCase() : '';
|
|
86
|
+
|
|
87
|
+
type LimiterOptions = {
|
|
88
|
+
windowMs: number;
|
|
89
|
+
limit: number;
|
|
90
|
+
keyGenerator?: (req: Request) => string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** A limiter backed by MongoDB. `name` must be unique. */
|
|
94
|
+
const createLimiter = (name: string, { windowMs, limit, keyGenerator = ip }: LimiterOptions) =>
|
|
95
|
+
rateLimit({
|
|
96
|
+
windowMs,
|
|
97
|
+
limit,
|
|
98
|
+
keyGenerator,
|
|
99
|
+
standardHeaders: 'draft-8',
|
|
100
|
+
legacyHeaders: false,
|
|
101
|
+
message,
|
|
102
|
+
store: new MongoStore(`${name}:`),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const MINUTE = 60 * 1000;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Coarse limit for the whole API. Kept in memory (per instance) so normal traffic
|
|
109
|
+
* doesn't pay for a database round trip; the auth limiters below are the strict ones.
|
|
110
|
+
*/
|
|
111
|
+
export const apiRateLimiter = rateLimit({
|
|
112
|
+
windowMs: MINUTE,
|
|
113
|
+
limit: 300,
|
|
114
|
+
standardHeaders: 'draft-8',
|
|
115
|
+
legacyHeaders: false,
|
|
116
|
+
message,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const rateLimiters = {
|
|
120
|
+
/** Account creation, per IP. */
|
|
121
|
+
signUp: createLimiter('sign-up', { windowMs: 60 * MINUTE, limit: 10 }),
|
|
122
|
+
|
|
123
|
+
/** Password guessing: per IP+email for one account, and per IP across many accounts. */
|
|
124
|
+
signIn: createLimiter('sign-in', {
|
|
125
|
+
windowMs: 15 * MINUTE,
|
|
126
|
+
limit: 10,
|
|
127
|
+
keyGenerator: (req) => `${ip(req)}:${email(req)}`,
|
|
128
|
+
}),
|
|
129
|
+
signInIp: createLimiter('sign-in-ip', { windowMs: 15 * MINUTE, limit: 50 }),
|
|
130
|
+
|
|
131
|
+
/** OTP / link / reset token guessing, per IP. */
|
|
132
|
+
verify: createLimiter('verify', { windowMs: 15 * MINUTE, limit: 20 }),
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Endpoints that send an email. Limited per recipient so nobody can flood an
|
|
136
|
+
* inbox from many IPs, and per IP so one client can't spray many addresses.
|
|
137
|
+
*/
|
|
138
|
+
emailPerAddress: createLimiter('email-address', {
|
|
139
|
+
windowMs: 15 * MINUTE,
|
|
140
|
+
limit: 3,
|
|
141
|
+
keyGenerator: email,
|
|
142
|
+
}),
|
|
143
|
+
emailPerIp: createLimiter('email-ip', { windowMs: 15 * MINUTE, limit: 10 }),
|
|
144
|
+
|
|
145
|
+
/** Password changes for signed-in users, per IP. */
|
|
146
|
+
changePassword: createLimiter('change-password', { windowMs: 15 * MINUTE, limit: 10 }),
|
|
147
|
+
|
|
148
|
+
/** Token refreshes, per IP (tokens are unguessable; this only caps abuse). */
|
|
149
|
+
refresh: createLimiter('refresh', { windowMs: 15 * MINUTE, limit: 300 }),
|
|
150
|
+
|
|
151
|
+
/** Starting a social login and exchanging its code, per IP. */
|
|
152
|
+
social: createLimiter('social', { windowMs: 15 * MINUTE, limit: 30 }),
|
|
153
|
+
};
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { NextFunction, Request, Response } from 'express';
|
|
1
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
2
|
+
import type { ZodType } from 'zod';
|
|
2
3
|
|
|
3
|
-
const RequestValidator = (schema:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
const RequestValidator = (schema: ZodType) => {
|
|
5
|
+
return async (req: Request, _res: Response, next: NextFunction) => {
|
|
6
|
+
try {
|
|
7
|
+
req.body = await schema.parseAsync(req.body);
|
|
8
|
+
next();
|
|
9
|
+
} catch (err) {
|
|
10
|
+
next(err);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export default RequestValidator;
|
|
@@ -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;
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
|
|
6
|
+
const create_account = catchAsync(async (req, res) => {
|
|
7
|
+
const result = await account_services.create_account_into_db(req);
|
|
8
|
+
manageResponse(res, {
|
|
9
|
+
statusCode: 201,
|
|
10
|
+
success: true,
|
|
11
|
+
message: 'Account created successfully',
|
|
12
|
+
data: result,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const verify_account_using_otp = catchAsync(async (req, res) => {
|
|
17
|
+
const result = await account_services.verify_account_using_otp_into_db(req);
|
|
18
|
+
manageResponse(res, {
|
|
19
|
+
statusCode: 200,
|
|
20
|
+
success: true,
|
|
21
|
+
message: 'Otp verification successful',
|
|
22
|
+
data: result,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const verify_account_using_link = catchAsync(async (req, res) => {
|
|
27
|
+
const result = await account_services.verify_account_using_link_into_db(req);
|
|
28
|
+
manageResponse(res, {
|
|
29
|
+
statusCode: 200,
|
|
30
|
+
success: true,
|
|
31
|
+
message: 'Account verification successful',
|
|
32
|
+
data: result,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const login_user = catchAsync(async (req, res) => {
|
|
37
|
+
const user = await account_services.login_user_into_db(req);
|
|
38
|
+
const tokens = await createSession(res, user);
|
|
39
|
+
manageResponse(res, {
|
|
40
|
+
statusCode: 200,
|
|
41
|
+
success: true,
|
|
42
|
+
message: 'User logged in successfully',
|
|
43
|
+
data: tokens,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const get_user_account = catchAsync(async (req, res) => {
|
|
48
|
+
const result = await account_services.get_user_account_from_db(req);
|
|
49
|
+
|
|
50
|
+
manageResponse(res, {
|
|
51
|
+
statusCode: 200,
|
|
52
|
+
success: true,
|
|
53
|
+
message: 'Account fetched successfully',
|
|
54
|
+
data: result,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const change_password = catchAsync(async (req, res) => {
|
|
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);
|
|
62
|
+
manageResponse(res, {
|
|
63
|
+
statusCode: 200,
|
|
64
|
+
success: true,
|
|
65
|
+
message: 'Password changed successfully',
|
|
66
|
+
data: tokens,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const resend_otp_and_verification_link = catchAsync(async (req, res) => {
|
|
71
|
+
const result = await account_services.resend_otp_and_verification_link_from_db(req);
|
|
72
|
+
manageResponse(res, {
|
|
73
|
+
statusCode: 200,
|
|
74
|
+
success: true,
|
|
75
|
+
message: 'If the account exists and is not verified, a new OTP has been sent',
|
|
76
|
+
data: result,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
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);
|
|
82
|
+
manageResponse(res, {
|
|
83
|
+
statusCode: 200,
|
|
84
|
+
success: true,
|
|
85
|
+
message: 'If an account exists for this email, a password reset link has been sent',
|
|
86
|
+
data: result,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const reset_password_using_token = catchAsync(async (req, res) => {
|
|
91
|
+
const result = await account_services.reset_password_using_token_into_db(req);
|
|
92
|
+
manageResponse(res, {
|
|
93
|
+
statusCode: 200,
|
|
94
|
+
success: true,
|
|
95
|
+
message: 'Password reset successfully',
|
|
96
|
+
data: result,
|
|
97
|
+
});
|
|
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
|
+
|
|
142
|
+
export const account_controller = {
|
|
143
|
+
create_account,
|
|
144
|
+
login_user,
|
|
145
|
+
get_user_account,
|
|
146
|
+
change_password,
|
|
147
|
+
verify_account_using_otp,
|
|
148
|
+
resend_otp_and_verification_link,
|
|
149
|
+
verify_account_using_link,
|
|
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,
|
|
156
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Types } from 'mongoose';
|
|
2
|
+
|
|
3
|
+
export const ROLES = ['ADMIN', 'USER'] as const;
|
|
4
|
+
export type ROLE = (typeof ROLES)[number];
|
|
5
|
+
|
|
6
|
+
export const PROVIDERS = [
|
|
7
|
+
'LOCAL',
|
|
8
|
+
'GOOGLE',
|
|
9
|
+
'FACEBOOK',
|
|
10
|
+
'GITHUB',
|
|
11
|
+
'TWITTER',
|
|
12
|
+
'LINKEDIN',
|
|
13
|
+
'DISCORD',
|
|
14
|
+
] as const;
|
|
15
|
+
export type PROVIDER = (typeof PROVIDERS)[number];
|
|
16
|
+
|
|
17
|
+
export type T_account = {
|
|
18
|
+
email: string;
|
|
19
|
+
password: string | null;
|
|
20
|
+
// provider the account was created with
|
|
21
|
+
provider: PROVIDER;
|
|
22
|
+
role: ROLE;
|
|
23
|
+
isDeleted: boolean;
|
|
24
|
+
isAccountVerified: boolean;
|
|
25
|
+
// bumped on password change/reset and "sign out everywhere"; access tokens
|
|
26
|
+
// and reset links carrying an older version stop working
|
|
27
|
+
tokenVersion: number;
|
|
28
|
+
// email verification (the OTP is stored as an HMAC, never in plain text)
|
|
29
|
+
lastOtp: string | null;
|
|
30
|
+
lastOtpSendingTime: Date | null;
|
|
31
|
+
otpAttempts: number;
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
updatedAt: Date;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** A social identity linked to an account, found by the provider's stable user id. */
|
|
37
|
+
export type T_social_account = {
|
|
38
|
+
accountId: Types.ObjectId;
|
|
39
|
+
provider: PROVIDER;
|
|
40
|
+
providerUserId: string;
|
|
41
|
+
createdAt: Date;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Single-use code exchanged for an access token after a social login (hash only). */
|
|
45
|
+
export type T_social_login_code = {
|
|
46
|
+
_id: string; // sha256 of the code
|
|
47
|
+
accountId: Types.ObjectId;
|
|
48
|
+
expiresAt: Date;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Refresh tokens are opaque random strings; only their hash is stored. Every refresh
|
|
53
|
+
* rotates the token, and all tokens of one sign-in share a `familyId`, so a stolen
|
|
54
|
+
* token that gets reused can revoke the whole session.
|
|
55
|
+
*/
|
|
56
|
+
export type T_refresh_token = {
|
|
57
|
+
_id: string; // sha256 of the token
|
|
58
|
+
accountId: Types.ObjectId;
|
|
59
|
+
familyId: string;
|
|
60
|
+
// the account's tokenVersion when issued; a password change makes it stale
|
|
61
|
+
tokenVersion: number;
|
|
62
|
+
expiresAt: Date;
|
|
63
|
+
// set when the token is rotated; kept until expiry to detect reuse
|
|
64
|
+
usedAt: Date | null;
|
|
65
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
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';
|
|
8
|
+
|
|
9
|
+
const accountRouter = Router();
|
|
10
|
+
|
|
11
|
+
accountRouter.post(
|
|
12
|
+
'/sign-up',
|
|
13
|
+
rateLimiters.signUp,
|
|
14
|
+
rateLimiters.emailPerAddress,
|
|
15
|
+
RequestValidator(account_validation.sign_up),
|
|
16
|
+
account_controller.create_account,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
accountRouter.post(
|
|
20
|
+
'/sign-in',
|
|
21
|
+
rateLimiters.signInIp,
|
|
22
|
+
rateLimiters.signIn,
|
|
23
|
+
RequestValidator(account_validation.sign_in),
|
|
24
|
+
account_controller.login_user,
|
|
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
|
+
|
|
42
|
+
accountRouter.put(
|
|
43
|
+
'/verify-otp',
|
|
44
|
+
rateLimiters.verify,
|
|
45
|
+
RequestValidator(account_validation.verify_otp),
|
|
46
|
+
account_controller.verify_account_using_otp,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
accountRouter.put(
|
|
50
|
+
'/verify-link',
|
|
51
|
+
rateLimiters.verify,
|
|
52
|
+
RequestValidator(account_validation.verify_link),
|
|
53
|
+
account_controller.verify_account_using_link,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
accountRouter.get('/me', auth(), account_controller.get_user_account);
|
|
57
|
+
|
|
58
|
+
accountRouter.put(
|
|
59
|
+
'/change-password',
|
|
60
|
+
rateLimiters.changePassword,
|
|
61
|
+
auth(),
|
|
62
|
+
RequestValidator(account_validation.change_password),
|
|
63
|
+
account_controller.change_password,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
accountRouter.put(
|
|
67
|
+
'/resend-otp',
|
|
68
|
+
rateLimiters.emailPerIp,
|
|
69
|
+
rateLimiters.emailPerAddress,
|
|
70
|
+
RequestValidator(account_validation.resend_otp),
|
|
71
|
+
account_controller.resend_otp_and_verification_link,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
accountRouter.put(
|
|
75
|
+
'/forget-password',
|
|
76
|
+
rateLimiters.emailPerIp,
|
|
77
|
+
rateLimiters.emailPerAddress,
|
|
78
|
+
RequestValidator(account_validation.forget_password),
|
|
79
|
+
account_controller.forget_password_generate_reset_token,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
accountRouter.put(
|
|
83
|
+
'/reset-password',
|
|
84
|
+
rateLimiters.verify,
|
|
85
|
+
RequestValidator(account_validation.reset_pass),
|
|
86
|
+
account_controller.reset_password_using_token,
|
|
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
|
+
|
|
101
|
+
export default accountRouter;
|