@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.

Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +180 -0
  3. package/dist/common/decorators/Body.js +20 -0
  4. package/dist/common/decorators/Param.js +23 -0
  5. package/dist/common/decorators/Query.js +20 -0
  6. package/dist/common/decorators/Req.js +32 -0
  7. package/dist/common/decorators/User.js +17 -0
  8. package/dist/common/loggers/Winston.js +37 -0
  9. package/dist/common/pipes/ParseIntPipe.js +11 -0
  10. package/dist/common/pipes/Pipe.js +2 -0
  11. package/dist/common/pipes/ZodValidationPipe.js +26 -0
  12. package/dist/common/prisma/index.js +26 -0
  13. package/dist/common/redis/index.js +40 -0
  14. package/dist/common/util/index.js +13 -0
  15. package/dist/conf/config.js +4 -0
  16. package/dist/conf/index.js +60 -0
  17. package/dist/core/decorators/Controller.js +140 -0
  18. package/dist/core/decorators/Methods.js +54 -0
  19. package/dist/core/decorators/UseGuards.js +11 -0
  20. package/dist/core/decorators/UseHooks.js +11 -0
  21. package/dist/core/decorators/UseMiddleware.js +11 -0
  22. package/dist/core/discovery/index.js +30 -0
  23. package/dist/core/discovery/utility.js +5 -0
  24. package/dist/core/loader/index.js +23 -0
  25. package/dist/core/server.js +43 -0
  26. package/dist/index.js +40 -0
  27. package/dist/modules/auth/auth.controller.js +140 -0
  28. package/dist/modules/auth/auth.service.js +84 -0
  29. package/dist/modules/auth/auth.zod.schemas.js +11 -0
  30. package/dist/shared/middlewares/Auth.js +48 -0
  31. package/dist/shared/middlewares/Sanitizer.js +45 -0
  32. package/dist/shared/models/Context.js +22 -0
  33. package/dist/shared/models/Guard.js +5 -0
  34. package/dist/shared/models/Hook.js +7 -0
  35. package/dist/shared/models/Logger.js +6 -0
  36. package/dist/shared/models/Middleware.js +5 -0
  37. package/dist/shared/models/ServerResponse.js +14 -0
  38. package/dist/shared/repository/BaseController.js +107 -0
  39. package/dist/shared/repository/Logger.js +65 -0
  40. package/dist/shared/repository/Service.js +67 -0
  41. package/package.json +48 -0
  42. 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,5 @@
1
+ import { dirname } from "path";
2
+ import { fileURLToPath } from "url";
3
+ const __fileName = fileURLToPath(import.meta.url);
4
+ const __dirName = dirname(__fileName);
5
+ export { __dirName, __fileName };
@@ -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 };