@cristianrg/fastpress 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.
Potentially problematic release.
This version of @cristianrg/fastpress might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/dist/common/decorators/Body.js +20 -0
- package/dist/common/decorators/Param.js +23 -0
- package/dist/common/decorators/Query.js +20 -0
- package/dist/common/decorators/Req.js +32 -0
- package/dist/common/decorators/User.js +17 -0
- package/dist/common/loggers/Winston.js +37 -0
- package/dist/common/pipes/ParseIntPipe.js +11 -0
- package/dist/common/pipes/Pipe.js +2 -0
- package/dist/common/pipes/ZodValidationPipe.js +26 -0
- package/dist/common/prisma/index.js +26 -0
- package/dist/common/redis/index.js +40 -0
- package/dist/common/util/index.js +13 -0
- package/dist/conf/config.js +4 -0
- package/dist/conf/index.js +60 -0
- package/dist/core/decorators/Controller.js +140 -0
- package/dist/core/decorators/Methods.js +54 -0
- package/dist/core/decorators/UseGuards.js +11 -0
- package/dist/core/decorators/UseHooks.js +11 -0
- package/dist/core/decorators/UseMiddleware.js +11 -0
- package/dist/core/discovery/index.js +30 -0
- package/dist/core/discovery/utility.js +5 -0
- package/dist/core/loader/index.js +23 -0
- package/dist/core/server.js +43 -0
- package/dist/index.js +40 -0
- package/dist/modules/auth/auth.controller.js +140 -0
- package/dist/modules/auth/auth.service.js +84 -0
- package/dist/modules/auth/auth.zod.schemas.js +11 -0
- package/dist/shared/middlewares/Auth.js +48 -0
- package/dist/shared/middlewares/Sanitizer.js +45 -0
- package/dist/shared/models/Context.js +22 -0
- package/dist/shared/models/Guard.js +5 -0
- package/dist/shared/models/Hook.js +7 -0
- package/dist/shared/models/Logger.js +6 -0
- package/dist/shared/models/Middleware.js +5 -0
- package/dist/shared/models/ServerResponse.js +14 -0
- package/dist/shared/repository/BaseController.js +107 -0
- package/dist/shared/repository/Logger.js +65 -0
- package/dist/shared/repository/Service.js +67 -0
- package/package.json +48 -0
- package/prisma/schema.prisma +41 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import "reflect-metadata";
|
|
3
|
+
import { CONTROLLER_ROUTES } from "./Methods.js";
|
|
4
|
+
import { ServerResponse } from "../../shared/models/ServerResponse.js";
|
|
5
|
+
import { Context } from "../../shared/models/Context.js";
|
|
6
|
+
import { PARAM_METADATA_KEY } from "../../common/decorators/Param.js";
|
|
7
|
+
import logger from "../../shared/repository/Logger.js";
|
|
8
|
+
/**
|
|
9
|
+
* Get all route definitions for a controller class and its parent classes
|
|
10
|
+
*/
|
|
11
|
+
function getAllRoutes(constructor) {
|
|
12
|
+
const routes = [];
|
|
13
|
+
const currentRoutes = CONTROLLER_ROUTES.get(constructor) || [];
|
|
14
|
+
routes.push(...currentRoutes);
|
|
15
|
+
// Get routes from parent classes
|
|
16
|
+
let baseProto = Object.getPrototypeOf(constructor.prototype);
|
|
17
|
+
while (baseProto && baseProto.constructor !== Object) {
|
|
18
|
+
const baseConstructor = baseProto.constructor;
|
|
19
|
+
const baseRoutes = CONTROLLER_ROUTES.get(baseConstructor) || [];
|
|
20
|
+
for (const baseRoute of baseRoutes) {
|
|
21
|
+
const routeExists = routes.some(r => r.path === baseRoute.path &&
|
|
22
|
+
r.method === baseRoute.method);
|
|
23
|
+
if (!routeExists) {
|
|
24
|
+
routes.push(baseRoute);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Move up the prototype chain
|
|
28
|
+
baseProto = Object.getPrototypeOf(baseProto);
|
|
29
|
+
}
|
|
30
|
+
return routes;
|
|
31
|
+
}
|
|
32
|
+
const paramTypeStrategy = {
|
|
33
|
+
"param": (ctx, propertyKey) => propertyKey ? ctx.params[propertyKey] : ctx.params,
|
|
34
|
+
"query": (ctx, propertyKey) => propertyKey ? ctx.query[propertyKey] : ctx.query,
|
|
35
|
+
"body": (ctx, propertyKey) => propertyKey ? ctx.body[propertyKey] : ctx.body,
|
|
36
|
+
"user": (ctx) => ctx.user,
|
|
37
|
+
"request": (ctx) => ctx.req,
|
|
38
|
+
"response": (ctx) => ctx.res
|
|
39
|
+
};
|
|
40
|
+
async function handleParams(ctx, paramMetadata) {
|
|
41
|
+
const args = new Array(paramMetadata.length);
|
|
42
|
+
if (paramMetadata.length == 0) {
|
|
43
|
+
return [ctx];
|
|
44
|
+
}
|
|
45
|
+
for (const param of paramMetadata) {
|
|
46
|
+
let value;
|
|
47
|
+
const extractor = paramTypeStrategy[param.type];
|
|
48
|
+
if (!extractor) {
|
|
49
|
+
throw new Error(`Unsupported parameter type: ${param.type}`);
|
|
50
|
+
}
|
|
51
|
+
value = extractor(ctx, param.propertyKey);
|
|
52
|
+
if (param.pipes && param.pipes.length > 0) {
|
|
53
|
+
if (value === undefined && param.isOptional) {
|
|
54
|
+
args[param.index] = undefined;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
for (const PipeClass of param.pipes) {
|
|
58
|
+
const pipeInstance = (typeof PipeClass === "function" ? new PipeClass() : PipeClass);
|
|
59
|
+
value = await pipeInstance.transform(value, ctx);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
args[param.index] = value;
|
|
63
|
+
}
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Controller decorator to define a controller class and its route prefix. It also sets up the router for the controller based on the defined routes and their handlers.
|
|
68
|
+
* @param prefix The prefix to be added to all routes defined in this controller. For example, if you set the prefix to "/users" and you have a method decorated with `@Get("/")`, the full path for that route will be "/users/".
|
|
69
|
+
* @returns
|
|
70
|
+
*/
|
|
71
|
+
export function Controller(prefix) {
|
|
72
|
+
return (target) => {
|
|
73
|
+
const router = Router();
|
|
74
|
+
// Get routes for this controller class and all parent classes
|
|
75
|
+
const routes = getAllRoutes(target);
|
|
76
|
+
const instance = new target();
|
|
77
|
+
for (const route of routes) {
|
|
78
|
+
const guards = Reflect.getMetadata("custom:guards", target.prototype, route.handlerName) || [];
|
|
79
|
+
const guardInstances = guards.map((guard) => (typeof guard === "function" ? new guard() : guard));
|
|
80
|
+
const hooks = Reflect.getMetadata("custom:hooks", target.prototype, route.handlerName) || [];
|
|
81
|
+
const hookInstances = hooks.map((hook) => (typeof hook === "function" ? new hook() : hook));
|
|
82
|
+
const paramMetadata = Reflect.getMetadata(PARAM_METADATA_KEY, target.prototype, route.handlerName) || [];
|
|
83
|
+
const handler = async (req, res, next) => {
|
|
84
|
+
const ctx = new Context(req, res, next);
|
|
85
|
+
const timestamp = Date.now();
|
|
86
|
+
try {
|
|
87
|
+
const handlerFunction = instance[route.handlerName];
|
|
88
|
+
if (typeof handlerFunction !== 'function') {
|
|
89
|
+
return next(new Error(`Route handler '${route.handlerName}' not found in ${target.name}`));
|
|
90
|
+
}
|
|
91
|
+
logger.info(`${req.method.toUpperCase()} ${req.originalUrl}`);
|
|
92
|
+
for (const guard of guardInstances) {
|
|
93
|
+
const canActivateResult = await guard.canActivate(ctx);
|
|
94
|
+
if (canActivateResult instanceof Object && !canActivateResult.allowed) {
|
|
95
|
+
return res.status(canActivateResult.statusCode || 403).json(new ServerResponse(canActivateResult.statusCode || 403, canActivateResult.message || "Forbidden"));
|
|
96
|
+
}
|
|
97
|
+
else if (!canActivateResult) {
|
|
98
|
+
return res.status(403).json(new ServerResponse(403, "Forbidden"));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const args = await handleParams(ctx, paramMetadata);
|
|
102
|
+
for (const hook of hookInstances) {
|
|
103
|
+
const earlyResult = await hook.before(ctx);
|
|
104
|
+
if (earlyResult instanceof ServerResponse) {
|
|
105
|
+
return res.status(earlyResult.statusCode).json(earlyResult);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const result = await handlerFunction.call(instance, ...args);
|
|
109
|
+
let finalResult = result;
|
|
110
|
+
for (const hook of hookInstances) {
|
|
111
|
+
const hookResult = await hook.after(ctx, finalResult);
|
|
112
|
+
if (hookResult instanceof ServerResponse) {
|
|
113
|
+
finalResult = hookResult;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (finalResult instanceof ServerResponse) {
|
|
117
|
+
res.status(finalResult.statusCode).json(finalResult);
|
|
118
|
+
}
|
|
119
|
+
else if (finalResult !== undefined) {
|
|
120
|
+
res.json(finalResult);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof ServerResponse) {
|
|
125
|
+
logger.error(`Error in ${target.name}.${route.handlerName}: ${error.message}`);
|
|
126
|
+
return res.status(error.statusCode).json(error);
|
|
127
|
+
}
|
|
128
|
+
next(error);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
const elapsed = Date.now() - timestamp;
|
|
132
|
+
logger.info(`${req.method.toUpperCase()} ${req.originalUrl} ${res.statusCode} - ${elapsed}ms`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
router[route.method](route.path, handler);
|
|
136
|
+
}
|
|
137
|
+
Reflect.defineMetadata("router", router, target);
|
|
138
|
+
Reflect.defineMetadata("prefix", prefix, target);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
// Use a Map to store routes for each controller class
|
|
3
|
+
// The key is the controller's constructor function and the value is an array of routes
|
|
4
|
+
export const CONTROLLER_ROUTES = new Map();
|
|
5
|
+
function createMethodDecorator(method) {
|
|
6
|
+
return (path) => {
|
|
7
|
+
return (target, propertyKey, descriptor) => {
|
|
8
|
+
// Get the actual class constructor
|
|
9
|
+
// If target is a prototype, target.constructor is the class constructor
|
|
10
|
+
// Otherwise, it's a static method and target is the constructor itself
|
|
11
|
+
const constructor = typeof target === 'function' ? target : target.constructor;
|
|
12
|
+
// Get existing routes for this controller class or initialize a new array
|
|
13
|
+
const routes = CONTROLLER_ROUTES.get(constructor) || [];
|
|
14
|
+
// Check if this route already exists to avoid duplicates
|
|
15
|
+
const routeExists = routes.some(r => r.path === path &&
|
|
16
|
+
r.method === method &&
|
|
17
|
+
r.handlerName === propertyKey);
|
|
18
|
+
if (!routeExists) {
|
|
19
|
+
// Add the new route
|
|
20
|
+
routes.push({
|
|
21
|
+
path,
|
|
22
|
+
method,
|
|
23
|
+
handlerName: propertyKey
|
|
24
|
+
});
|
|
25
|
+
// Store the updated routes back in the map
|
|
26
|
+
CONTROLLER_ROUTES.set(constructor, routes);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get decorator. Adds a GET route to the controller.
|
|
33
|
+
* @param path The route path.
|
|
34
|
+
* @returns `ServerResponse` or `Promise<ServerResponse>` from the decorated method.
|
|
35
|
+
*/
|
|
36
|
+
export const Get = createMethodDecorator("get");
|
|
37
|
+
/**
|
|
38
|
+
* Post decorator. Adds a POST route to the controller.
|
|
39
|
+
* @param path The route path.
|
|
40
|
+
* @returns `ServerResponse` or `Promise<ServerResponse>` from the decorated method.
|
|
41
|
+
*/
|
|
42
|
+
export const Post = createMethodDecorator("post");
|
|
43
|
+
/**
|
|
44
|
+
* Put decorator. Adds a PUT route to the controller.
|
|
45
|
+
* @param path The route path.
|
|
46
|
+
* @returns `ServerResponse` or `Promise<ServerResponse>` from the decorated method.
|
|
47
|
+
*/
|
|
48
|
+
export const Put = createMethodDecorator("put");
|
|
49
|
+
/**
|
|
50
|
+
* Delete decorator. Adds a DELETE route to the controller.
|
|
51
|
+
* @param path The route path.
|
|
52
|
+
* @returns `ServerResponse` or `Promise<ServerResponse>` from the decorated method.
|
|
53
|
+
*/
|
|
54
|
+
export const Delete = createMethodDecorator("delete");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard decorator to apply guards to a route handler. Guards are functions that can be executed before the route handler is called, allowing you to perform authorization checks, validate user permissions, etc.
|
|
3
|
+
* @param guards An array of Guard instances to be applied to the route handler. The guards will be executed in the order they are provided in the array.
|
|
4
|
+
* @returns
|
|
5
|
+
*/
|
|
6
|
+
export function UseGuards(...guards) {
|
|
7
|
+
return (target, propertyKey, descriptor) => {
|
|
8
|
+
const existingGuards = Reflect.getMetadata("custom:guards", target, propertyKey) || [];
|
|
9
|
+
Reflect.defineMetadata("custom:guards", [...existingGuards, ...guards], target, propertyKey);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook decorator to apply hooks to a route handler. Hooks are functions that can be executed before or after the route handler is called, allowing you to perform additional logic such as logging, modifying the request/response, etc.
|
|
3
|
+
* @param hooks An array of Hook instances to be applied to the route handler. The hooks will be executed in the order they are provided in the array.
|
|
4
|
+
* @returns
|
|
5
|
+
*/
|
|
6
|
+
export function UseHooks(...hooks) {
|
|
7
|
+
return (target, propertyKey, descriptor) => {
|
|
8
|
+
const existingHooks = Reflect.getMetadata("custom:hooks", target, propertyKey) || [];
|
|
9
|
+
Reflect.defineMetadata("custom:hooks", [...existingHooks, ...hooks], target, propertyKey);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware decorator to apply middlewares to a controller class. Middlewares are functions that can be executed before the route handlers of the controller are called, allowing you to perform tasks such as authentication, logging, etc.
|
|
3
|
+
* @param middlewares An array of Middleware instances to be applied to the controller class. The middlewares will be executed in the order they are provided in the array.
|
|
4
|
+
* @returns
|
|
5
|
+
*/
|
|
6
|
+
export function UseMiddleware(...middlewares) {
|
|
7
|
+
return (target) => {
|
|
8
|
+
const existingMiddlewares = Reflect.getMetadata("custom:middlewares", target) || [];
|
|
9
|
+
Reflect.defineMetadata("custom:middlewares", [...existingMiddlewares, ...middlewares], target);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { glob } from "glob";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import { __dirName, __fileName } from "./utility.js";
|
|
5
|
+
export const discovery = async () => {
|
|
6
|
+
// Determine the file extension based on the current file's extension (either .ts or .js)
|
|
7
|
+
const extension = __fileName.endsWith(".js") ? "js" : "ts";
|
|
8
|
+
const controllersPath = path.resolve(__dirName, "../../modules");
|
|
9
|
+
const internalControllerPaths = path.resolve(__dirName, "../modules");
|
|
10
|
+
const controllers = await glob(`${controllersPath}/**/**.controller.${extension}`);
|
|
11
|
+
const internalControllers = await glob(`${internalControllerPaths}/**/**.controller.${extension}`);
|
|
12
|
+
const modules = [];
|
|
13
|
+
for (const controllerPath of internalControllers) {
|
|
14
|
+
const absolutePath = path.resolve(controllerPath);
|
|
15
|
+
const fileURL = pathToFileURL(absolutePath).href;
|
|
16
|
+
const module = await import(fileURL);
|
|
17
|
+
if (module.default) {
|
|
18
|
+
modules.push(module.default);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const controllerPath of controllers) {
|
|
22
|
+
const absolutePath = path.resolve(controllerPath);
|
|
23
|
+
const fileURL = pathToFileURL(absolutePath).href;
|
|
24
|
+
const module = await import(fileURL);
|
|
25
|
+
if (module.default) {
|
|
26
|
+
modules.push(module.default);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return modules;
|
|
30
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { Context } from "../../shared/models/Context.js";
|
|
3
|
+
export function registerControllers(app, controllers) {
|
|
4
|
+
for (const controller of controllers) {
|
|
5
|
+
const controllerPrefix = Reflect.getMetadata("prefix", controller);
|
|
6
|
+
const router = Reflect.getMetadata("router", controller);
|
|
7
|
+
const middlewares = Reflect.getMetadata("custom:middlewares", controller) || [];
|
|
8
|
+
if (router) {
|
|
9
|
+
if (middlewares.length > 0) {
|
|
10
|
+
app.use(controllerPrefix, ...middlewares.map(mw => {
|
|
11
|
+
const middlewareInstance = typeof mw === "function" ? new mw() : mw;
|
|
12
|
+
return (req, res, next) => {
|
|
13
|
+
const ctx = new Context(req, res, next);
|
|
14
|
+
return middlewareInstance.handle(ctx);
|
|
15
|
+
};
|
|
16
|
+
}), router);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
app.use(controllerPrefix, router);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import conf, { initializeConfig } from "../conf/index.js";
|
|
4
|
+
import { registerControllers } from "./loader/index.js";
|
|
5
|
+
import { discovery } from "./discovery/index.js";
|
|
6
|
+
import { initializePrisma } from "../common/prisma/index.js";
|
|
7
|
+
const corsOptions = {
|
|
8
|
+
origin: (origin, callback) => {
|
|
9
|
+
if (!origin || conf.ALLOWED_ORIGINS.includes(origin))
|
|
10
|
+
callback(null, true);
|
|
11
|
+
else
|
|
12
|
+
callback(new Error("Not allowed by CORS"), false);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const bootstrap = async () => {
|
|
16
|
+
const modules = await discovery();
|
|
17
|
+
const app = express();
|
|
18
|
+
app.use(express.json());
|
|
19
|
+
app.use(express.urlencoded({ extended: true }));
|
|
20
|
+
app.use(cors(corsOptions));
|
|
21
|
+
registerControllers(app, modules);
|
|
22
|
+
return app;
|
|
23
|
+
};
|
|
24
|
+
const createServer = async (onStart) => {
|
|
25
|
+
let app;
|
|
26
|
+
try {
|
|
27
|
+
await initializeConfig();
|
|
28
|
+
initializePrisma();
|
|
29
|
+
app = await bootstrap();
|
|
30
|
+
const port = conf.PORT || 3000;
|
|
31
|
+
app.listen(port, () => {
|
|
32
|
+
console.log(`Amazing! \nYour server is running on port ${port}`);
|
|
33
|
+
if (onStart)
|
|
34
|
+
onStart(app);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error("Failed to start the server:", error);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
return app;
|
|
42
|
+
};
|
|
43
|
+
export { createServer };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Core Decorators
|
|
2
|
+
export * from './core/decorators/Controller.js';
|
|
3
|
+
export * from './core/decorators/Methods.js';
|
|
4
|
+
export * from './core/decorators/UseMiddleware.js';
|
|
5
|
+
export * from './core/decorators/UseGuards.js';
|
|
6
|
+
export * from './core/decorators/UseHooks.js';
|
|
7
|
+
// Models
|
|
8
|
+
export * from './shared/models/Context.js';
|
|
9
|
+
export * from './shared/models/Guard.js';
|
|
10
|
+
export * from './shared/models/Hook.js';
|
|
11
|
+
export * from './shared/models/Logger.js';
|
|
12
|
+
export * from './shared/models/Middleware.js';
|
|
13
|
+
export * from './shared/models/ServerResponse.js';
|
|
14
|
+
// Middlewares
|
|
15
|
+
export * from './shared/middlewares/Auth.js';
|
|
16
|
+
export * from './shared/middlewares/Sanitizer.js';
|
|
17
|
+
// Repository
|
|
18
|
+
export * from './shared/repository/BaseController.js';
|
|
19
|
+
export * from './shared/repository/Service.js';
|
|
20
|
+
export * from './shared/repository/Logger.js';
|
|
21
|
+
// Common decorators
|
|
22
|
+
export * from './common/decorators/Body.js';
|
|
23
|
+
export * from './common/decorators/Param.js';
|
|
24
|
+
export * from './common/decorators/Query.js';
|
|
25
|
+
export * from './common/decorators/Req.js';
|
|
26
|
+
export * from './common/decorators/User.js';
|
|
27
|
+
// Common loggers
|
|
28
|
+
export * from './common/loggers/Winston.js';
|
|
29
|
+
// Common pipes
|
|
30
|
+
export * from './common/pipes/ParseIntPipe.js';
|
|
31
|
+
export * from './common/pipes/ZodValidationPipe.js';
|
|
32
|
+
export * from './common/pipes/Pipe.js'; // Pipe model
|
|
33
|
+
// Prisma instance
|
|
34
|
+
export { default as prisma, getPrisma, initializePrisma } from './common/prisma/index.js';
|
|
35
|
+
// Redis instance
|
|
36
|
+
export * from './common/redis/index.js';
|
|
37
|
+
// Utilities
|
|
38
|
+
export * from './common/util/index.js';
|
|
39
|
+
// Core
|
|
40
|
+
export * from './core/server.js';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { Body } from "../../common/decorators/Body.js";
|
|
14
|
+
import { Req } from "../../common/decorators/Req.js";
|
|
15
|
+
import { ZodValidationPipe } from "../../common/pipes/ZodValidationPipe.js";
|
|
16
|
+
import { Controller } from "../../core/decorators/Controller.js";
|
|
17
|
+
import { Get, Post } from "../../core/decorators/Methods.js";
|
|
18
|
+
import { LoginUserSchema, SignupUserSchema } from "./auth.zod.schemas.js";
|
|
19
|
+
import { AuthService } from "./auth.service.js";
|
|
20
|
+
import { ServerResponse } from "../../shared/models/ServerResponse.js";
|
|
21
|
+
import logger from "../../shared/repository/Logger.js";
|
|
22
|
+
import conf from "../../conf/index.js";
|
|
23
|
+
let AuthController = class AuthController {
|
|
24
|
+
async login(body, req) {
|
|
25
|
+
const { email, password } = body;
|
|
26
|
+
const sessionId = req.headers.cookie?.split(';').find(cookie => cookie.trim().startsWith('session='))?.split('=')[1] || undefined;
|
|
27
|
+
try {
|
|
28
|
+
const user = await AuthService.login(email, password);
|
|
29
|
+
const { jwt } = await AuthService.generateAccessToken(user.id);
|
|
30
|
+
const { rjwt } = await AuthService.generateRefreshToken(user.id, sessionId);
|
|
31
|
+
req.res?.cookie("jwt", jwt.token, {
|
|
32
|
+
httpOnly: true,
|
|
33
|
+
secure: conf.ENV === "production",
|
|
34
|
+
sameSite: "strict",
|
|
35
|
+
maxAge: jwt.expiresIn
|
|
36
|
+
});
|
|
37
|
+
req.res?.cookie("rjwt", rjwt.token, {
|
|
38
|
+
httpOnly: true,
|
|
39
|
+
secure: conf.ENV === "production",
|
|
40
|
+
sameSite: "strict",
|
|
41
|
+
maxAge: rjwt.expiresIn
|
|
42
|
+
});
|
|
43
|
+
if (!sessionId) {
|
|
44
|
+
req.res?.cookie("session", rjwt.token, {
|
|
45
|
+
httpOnly: true,
|
|
46
|
+
secure: conf.ENV === "production",
|
|
47
|
+
sameSite: "strict",
|
|
48
|
+
maxAge: rjwt.expiresIn
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return new ServerResponse(200, "Login successful", { user });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
logger.error(`Login error for email ${email}: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
return new ServerResponse(401, "Invalid email or password");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async signup(body, req) {
|
|
59
|
+
const { email, password, name } = body;
|
|
60
|
+
try {
|
|
61
|
+
const user = await AuthService.signup(email, password, name);
|
|
62
|
+
const { jwt } = await AuthService.generateAccessToken(user.id);
|
|
63
|
+
const { rjwt } = await AuthService.generateRefreshToken(user.id);
|
|
64
|
+
req.res?.cookie("jwt", jwt.token, {
|
|
65
|
+
httpOnly: true,
|
|
66
|
+
secure: conf.ENV === "production",
|
|
67
|
+
sameSite: "strict",
|
|
68
|
+
maxAge: jwt.expiresIn
|
|
69
|
+
});
|
|
70
|
+
req.res?.cookie("rjwt", rjwt.token, {
|
|
71
|
+
httpOnly: true,
|
|
72
|
+
secure: conf.ENV === "production",
|
|
73
|
+
sameSite: "strict",
|
|
74
|
+
maxAge: rjwt.expiresIn
|
|
75
|
+
});
|
|
76
|
+
req.res?.cookie("session", rjwt.token, {
|
|
77
|
+
httpOnly: true,
|
|
78
|
+
secure: conf.ENV === "production",
|
|
79
|
+
sameSite: "strict",
|
|
80
|
+
maxAge: rjwt.expiresIn
|
|
81
|
+
});
|
|
82
|
+
return new ServerResponse(201, "Signup successful", { user });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
logger.error(`Signup error for email ${email}: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
return new ServerResponse(400, error instanceof Error ? error.message : "Signup failed");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async refresh(req) {
|
|
90
|
+
try {
|
|
91
|
+
const refreshToken = req.headers.cookie?.split(';').find(cookie => cookie.trim().startsWith('rjwt='))?.split('=')[1];
|
|
92
|
+
if (!refreshToken) {
|
|
93
|
+
return new ServerResponse(401, "No refresh token provided");
|
|
94
|
+
}
|
|
95
|
+
const user = await AuthService.validateRefreshToken(refreshToken);
|
|
96
|
+
if (!user) {
|
|
97
|
+
return new ServerResponse(401, "Invalid refresh token");
|
|
98
|
+
}
|
|
99
|
+
const { jwt } = await AuthService.generateAccessToken(user.id);
|
|
100
|
+
req.res?.cookie("jwt", jwt.token, {
|
|
101
|
+
httpOnly: true,
|
|
102
|
+
secure: conf.ENV === "production",
|
|
103
|
+
sameSite: "strict",
|
|
104
|
+
maxAge: jwt.expiresIn
|
|
105
|
+
});
|
|
106
|
+
return new ServerResponse(200, "Token refreshed", { user });
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
logger.error(`Refresh error: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
return new ServerResponse(401, "Invalid refresh token");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
__decorate([
|
|
115
|
+
Post("/login"),
|
|
116
|
+
__param(0, Body(undefined, new ZodValidationPipe(LoginUserSchema))),
|
|
117
|
+
__param(1, Req()),
|
|
118
|
+
__metadata("design:type", Function),
|
|
119
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
120
|
+
__metadata("design:returntype", Promise)
|
|
121
|
+
], AuthController.prototype, "login", null);
|
|
122
|
+
__decorate([
|
|
123
|
+
Post("/signup"),
|
|
124
|
+
__param(0, Body(undefined, new ZodValidationPipe(SignupUserSchema))),
|
|
125
|
+
__param(1, Req()),
|
|
126
|
+
__metadata("design:type", Function),
|
|
127
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
128
|
+
__metadata("design:returntype", Promise)
|
|
129
|
+
], AuthController.prototype, "signup", null);
|
|
130
|
+
__decorate([
|
|
131
|
+
Get("/refresh"),
|
|
132
|
+
__param(0, Req()),
|
|
133
|
+
__metadata("design:type", Function),
|
|
134
|
+
__metadata("design:paramtypes", [Object]),
|
|
135
|
+
__metadata("design:returntype", Promise)
|
|
136
|
+
], AuthController.prototype, "refresh", null);
|
|
137
|
+
AuthController = __decorate([
|
|
138
|
+
Controller("/auth")
|
|
139
|
+
], AuthController);
|
|
140
|
+
export default AuthController;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import prisma from "../../common/prisma/index.js";
|
|
2
|
+
import { compare, encrypt } from "../../common/util/index.js";
|
|
3
|
+
import conf from "../../conf/index.js";
|
|
4
|
+
import jwtoken from "jsonwebtoken";
|
|
5
|
+
class AuthService {
|
|
6
|
+
static async login(email, password) {
|
|
7
|
+
const user = await prisma.user.findUnique({ where: { email } });
|
|
8
|
+
if (!user) {
|
|
9
|
+
throw new Error("User not found");
|
|
10
|
+
}
|
|
11
|
+
const isPasswordValid = compare(password, user.password);
|
|
12
|
+
if (!isPasswordValid) {
|
|
13
|
+
throw new Error("Invalid password");
|
|
14
|
+
}
|
|
15
|
+
return user;
|
|
16
|
+
}
|
|
17
|
+
static async signup(email, password, name) {
|
|
18
|
+
const existingUser = await prisma.user.findUnique({ where: { email } });
|
|
19
|
+
if (existingUser) {
|
|
20
|
+
throw new Error("Email already in use");
|
|
21
|
+
}
|
|
22
|
+
const encryptedPassword = encrypt(password);
|
|
23
|
+
const newUser = await prisma.user.create({
|
|
24
|
+
data: {
|
|
25
|
+
email,
|
|
26
|
+
password: encryptedPassword,
|
|
27
|
+
name
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return newUser;
|
|
31
|
+
}
|
|
32
|
+
static async generateRefreshToken(userId, sessionId) {
|
|
33
|
+
const user = await prisma.user.findUnique({ where: { id: userId } });
|
|
34
|
+
if (!user) {
|
|
35
|
+
throw new Error("User not found");
|
|
36
|
+
}
|
|
37
|
+
const rjwt = jwtoken.sign({ id: user.id }, conf.JWT_SECRET, { algorithm: conf.ALGORITHM, expiresIn: conf.RJWT_EXPIRATION });
|
|
38
|
+
const rjwtObject = {
|
|
39
|
+
token: rjwt,
|
|
40
|
+
expiresIn: conf.RJWT_EXPIRATION
|
|
41
|
+
};
|
|
42
|
+
const existingSession = sessionId ? await prisma.session.findUnique({ where: { id: sessionId } }) : null;
|
|
43
|
+
if (!existingSession) {
|
|
44
|
+
await prisma.session.create({
|
|
45
|
+
data: {
|
|
46
|
+
token: rjwt,
|
|
47
|
+
userId: user.id,
|
|
48
|
+
expiresAt: new Date(Date.now() + conf.RJWT_EXPIRATION)
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await prisma.session.update({
|
|
54
|
+
where: { id: existingSession.id },
|
|
55
|
+
data: {
|
|
56
|
+
token: rjwt,
|
|
57
|
+
expiresAt: new Date(Date.now() + conf.RJWT_EXPIRATION)
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return { rjwt: rjwtObject };
|
|
62
|
+
}
|
|
63
|
+
static async generateAccessToken(userId) {
|
|
64
|
+
const user = await prisma.user.findUnique({ where: { id: userId } });
|
|
65
|
+
if (!user) {
|
|
66
|
+
throw new Error("User not found");
|
|
67
|
+
}
|
|
68
|
+
const jwt = jwtoken.sign({ id: user.id }, conf.JWT_SECRET, { algorithm: conf.ALGORITHM, expiresIn: conf.JWT_EXPIRATION });
|
|
69
|
+
const jwtObject = {
|
|
70
|
+
token: jwt,
|
|
71
|
+
expiresIn: conf.JWT_EXPIRATION
|
|
72
|
+
};
|
|
73
|
+
return { jwt: jwtObject };
|
|
74
|
+
}
|
|
75
|
+
static async validateRefreshToken(token) {
|
|
76
|
+
jwtoken.verify(token, conf.JWT_SECRET, { algorithms: [conf.ALGORITHM] });
|
|
77
|
+
const session = await prisma.session.findUnique({ where: { token }, include: { user: { omit: { password: true } } } });
|
|
78
|
+
if (!session) {
|
|
79
|
+
throw new Error("Session not found");
|
|
80
|
+
}
|
|
81
|
+
return session.user;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export { AuthService };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
const LoginUserSchema = z.object({
|
|
3
|
+
email: z.string({ error: (iss) => iss.input === undefined ? "Email is required" : iss.message }).email(),
|
|
4
|
+
password: z.string({ error: (iss) => iss.input === undefined ? "Password is required" : iss.message }).min(6)
|
|
5
|
+
});
|
|
6
|
+
const SignupUserSchema = z.object({
|
|
7
|
+
email: z.string({ error: (iss) => iss.input === undefined ? "Email is required" : iss.message }).email(),
|
|
8
|
+
password: z.string({ error: (iss) => iss.input === undefined ? "Password is required" : iss.message }).min(6),
|
|
9
|
+
name: z.string({ error: (iss) => iss.input === undefined ? "Name is required" : iss.message }).min(2)
|
|
10
|
+
});
|
|
11
|
+
export { LoginUserSchema, SignupUserSchema };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Middleware } from "../models/Middleware.js";
|
|
2
|
+
import jwt from "jsonwebtoken";
|
|
3
|
+
import conf from "../../conf/index.js";
|
|
4
|
+
import { getRedisClient } from "../../common/redis/index.js";
|
|
5
|
+
import { ServerResponse } from "../models/ServerResponse.js";
|
|
6
|
+
import prisma from "../../common/prisma/index.js";
|
|
7
|
+
const { ALGORITHM } = conf;
|
|
8
|
+
/**
|
|
9
|
+
* Auth middleware to protect routes that require authentication. It checks for the presence of a JWT token in the cookies, verifies it, and attaches the authenticated user to the request context. If the token is missing, invalid, or expired, it responds with a 401 Unauthorized status.
|
|
10
|
+
*/
|
|
11
|
+
class Auth extends Middleware {
|
|
12
|
+
async handle(ctx) {
|
|
13
|
+
const accessToken = ctx.req.headers.cookie?.split(';').find(cookie => cookie.trim().startsWith('jwt='))?.split('=')[1];
|
|
14
|
+
if (!accessToken) {
|
|
15
|
+
ctx.res.status(401).json(new ServerResponse(401, "No token provided"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const decoded = jwt.verify(accessToken, conf.JWT_SECRET, {
|
|
20
|
+
algorithms: [ALGORITHM]
|
|
21
|
+
});
|
|
22
|
+
const redisClient = await getRedisClient();
|
|
23
|
+
let cachedUser = null;
|
|
24
|
+
if (redisClient) {
|
|
25
|
+
cachedUser = await redisClient.get(`user:${decoded.id}`);
|
|
26
|
+
}
|
|
27
|
+
if (cachedUser) {
|
|
28
|
+
ctx.user = JSON.parse(cachedUser);
|
|
29
|
+
ctx.next();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const dbUser = await prisma.user.findUnique({ where: { id: decoded.id }, omit: { password: true } });
|
|
33
|
+
if (!dbUser)
|
|
34
|
+
throw new Error("User not found");
|
|
35
|
+
if (redisClient) {
|
|
36
|
+
await redisClient.set(`user:${decoded.id}`, JSON.stringify(dbUser), { EX: 3600 });
|
|
37
|
+
}
|
|
38
|
+
ctx.user = dbUser;
|
|
39
|
+
ctx.req.user = dbUser;
|
|
40
|
+
ctx.next();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error.name === 'TokenExpiredError' ? 'Token Expired' : 'Unauthorized';
|
|
44
|
+
ctx.res.status(401).json(new ServerResponse(401, message));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export { Auth };
|