Skip to content

Commit

Permalink
Merge pull request #24 from fless-lab/feat/advanced-f-structure
Browse files Browse the repository at this point in the history
feat: Introduced core plugins and middlewares for authentication and user context
  • Loading branch information
fless-lab authored Sep 3, 2024
2 parents f6cc853 + 67b6787 commit 020ff65
Show file tree
Hide file tree
Showing 28 changed files with 635 additions and 43 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"{}": false
}
}
]
],
"@typescript-eslint/ban-ts-comment": "warn"
},
"ignorePatterns": ["node_modules/", "dist/", "build/"]
}
4 changes: 4 additions & 0 deletions api.rest
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
https://documenter.getpostman.com/view/15120939/2sA3kUGMx7


Feel free to let comments is something going wrong
23 changes: 23 additions & 0 deletions src/apps/auth/models/_plugins/attemp-limiting.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Ceci c'est juste un exemple de plugin
*/

import { Schema } from 'mongoose';

export const attemptLimitingPlugin = (
schema: Schema,
options?: { maxAttempts?: number },
) => {
schema.add({ attempts: { type: Number, default: 0 } });

schema.methods.incrementAttempts = async function () {
this.attempts += 1;
const maxAttempts = options?.maxAttempts || 3;

if (this.attempts >= maxAttempts) {
this.used = true;
this.isFresh = false;
}
await this.save();
};
};
1 change: 1 addition & 0 deletions src/apps/auth/models/_plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './attemp-limiting.plugin';
20 changes: 16 additions & 4 deletions src/apps/auth/models/otp.model.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Schema, model } from 'mongoose';
import { IOTPModel } from '../types';
import { config } from '../../../core/config';
import { Schema } from 'mongoose';
import { BaseModel, createBaseSchema } from '../../../core/engine';
import { attemptLimitingPlugin } from './_plugins';

const otpSchema: Schema = new Schema(
const OTP_MODEL_NAME = 'OTP';

const otpSchema = createBaseSchema<IOTPModel>(
{
code: {
type: String,
Expand Down Expand Up @@ -30,10 +34,18 @@ const otpSchema: Schema = new Schema(
enum: Object.keys(config.otp.purposes),
required: true,
},
attempts: {
type: Number,
default: 0,
},
},
{
excludePlugins: ['softDelete'],
includePlugins: [[attemptLimitingPlugin, { maxAttempts: 5 }]],
modelName: OTP_MODEL_NAME,
},
{ timestamps: true },
);

const OTPModel = model<IOTPModel>('OTP', otpSchema);
const OTPModel = new BaseModel<IOTPModel>(OTP_MODEL_NAME, otpSchema).getModel();

export default OTPModel;
10 changes: 5 additions & 5 deletions src/apps/auth/types/otp.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Document } from 'mongoose';
import { Document, Types } from 'mongoose';
import { config } from '../../../core/config';
import { IBaseModel } from '../../../core/engine';

export type TOTPPurpose = keyof typeof config.otp.purposes;

export interface IOTP {
code: string;
user: string;
user: Types.ObjectId;
used: boolean;
isFresh: boolean;
expiresAt: Date;
purpose: TOTPPurpose;
createdAt?: Date;
updatedAt?: Date;
attempts?: number;
}

export interface IOTPModel extends IOTP, Document {}
export interface IOTPModel extends IOTP, IBaseModel, Document {}
19 changes: 13 additions & 6 deletions src/apps/users/models/user.model.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Schema, model, CallbackError } from 'mongoose';
import { CallbackError } from 'mongoose';
import bcrypt from 'bcrypt';
import { IUserModel } from '../types';
import { BaseModel, createBaseSchema } from '../../../core/engine';
import { config } from '../../../core/config';

const UserSchema: Schema = new Schema(
const USER_MODEL_NAME = 'User';

const UserSchema = createBaseSchema<IUserModel>(
{
firstname: { type: String, required: true },
lastname: { type: String, required: true },
Expand All @@ -14,15 +17,16 @@ const UserSchema: Schema = new Schema(
active: { type: Boolean, default: true },
verified: { type: Boolean, default: false },
},
{ timestamps: true },
{
modelName: USER_MODEL_NAME,
},
);

// Pre-hook for hashing the password before saving
UserSchema.pre('save', async function (next) {
try {
if (this.isNew || this.isModified('password')) {
const salt = await bcrypt.genSalt(config.bcrypt.saltRounds);
const hashedPassword = await bcrypt.hash(this.password as string, salt);
const hashedPassword = await bcrypt.hash(this.password, salt);
this.password = hashedPassword;
}
next();
Expand All @@ -31,6 +35,9 @@ UserSchema.pre('save', async function (next) {
}
});

const UserModel = model<IUserModel>('User', UserSchema);
const UserModel = new BaseModel<IUserModel>(
USER_MODEL_NAME,
UserSchema,
).getModel();

export default UserModel;
18 changes: 15 additions & 3 deletions src/apps/users/routes/user.routes.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { Router } from 'express';
import { authenticateRequest, validate } from '../../../common/shared';
import {
authenticateAndAttachUserContext,
validate,
} from '../../../common/shared';
import { createUserSchema } from '../validators';
import { UserController } from '../controllers';
const router = Router();

router.post('/', validate(createUserSchema), UserController.createUser);
router.post(
'/',
authenticateAndAttachUserContext,
validate(createUserSchema),
UserController.createUser,
);
router.get('/', UserController.getAllUsers);
router.get('/current', authenticateRequest, UserController.getCurrentUser);
router.get(
'/current',
authenticateAndAttachUserContext,
UserController.getCurrentUser,
);
router.get('/:id', UserController.getUserById);

export default router;
9 changes: 5 additions & 4 deletions src/apps/users/types/user.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// src/apps/users/types/user.ts

import { Document } from 'mongoose';
import { IBaseModel } from '../../../core/engine'; // Importer IBaseModel pour l'extension

export type TUserRole = 'admin' | 'user' | 'guest';

export interface IUser {
export interface IUser extends IBaseModel {
firstname: string;
lastname: string;
email: string;
Expand All @@ -11,8 +14,6 @@ export interface IUser {
profilePhoto?: string;
verified: boolean;
active: boolean;
createdAt?: Date;
updatedAt?: Date;
}

export interface IUserModel extends IUser, Document {}
export interface IUserModel extends IUser, IBaseModel, Document {}
25 changes: 25 additions & 0 deletions src/common/shared/middlewares/attach-user-to-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Request, Response, NextFunction } from 'express';
import { AsyncStorageService, logger } from '../services';

export const attachUserToContext = (
req: Request,
res: Response,
next: NextFunction,
) => {
const asyncStorage = AsyncStorageService.getInstance();
// @ts-ignore: Suppress TS error for non-existent property
const payload = req.payload;
if (payload && typeof payload.aud === 'string') {
const userId = payload.aud;

asyncStorage.run(() => {
asyncStorage.set('currentUserId', userId);
next();
});
} else {
logger.warn(
'Warning: Unable to attach user context, missing payload or audience field.',
);
next();
}
};
32 changes: 32 additions & 0 deletions src/common/shared/middlewares/authenticate-req-with-user-attach.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Request, Response, NextFunction } from 'express';
import { JwtService, AsyncStorageService, logger } from '../services';

export const authenticateAndAttachUserContext = (
req: Request,
res: Response,
next: NextFunction,
) => {
JwtService.verifyAccessToken(req, res, (authErr: any) => {
if (authErr) {
return next(authErr);
}

// @ts-ignore: Suppress TS error for non-existent property
const payload = req.payload;

if (payload && typeof payload.aud === 'string') {
const userId = payload.aud;
const asyncStorage = AsyncStorageService.getInstance();

asyncStorage.run(() => {
asyncStorage.set('currentUserId', userId);
next();
});
} else {
logger.warn(
'Warning: Unable to attach user context, missing payload or audience field.',
);
next();
}
});
};
1 change: 1 addition & 0 deletions src/common/shared/middlewares/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as authenticateRequest } from './authenticate-request';
export { default as bruteForceMiddleware } from './bruteforce';
export * from './rate-limiter';
export * from './validate';
export * from './authenticate-req-with-user-attach';
33 changes: 33 additions & 0 deletions src/common/shared/services/async-localstorage.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { AsyncLocalStorage } from 'async_hooks';

export class AsyncStorageService {
private static instance: AsyncStorageService;
private storage: AsyncLocalStorage<Map<string, any>>;

private constructor() {
this.storage = new AsyncLocalStorage();
}

public static getInstance(): AsyncStorageService {
if (!AsyncStorageService.instance) {
AsyncStorageService.instance = new AsyncStorageService();
}
return AsyncStorageService.instance;
}

public set(key: string, value: any) {
const store = this.storage.getStore();
if (store) {
store.set(key, value);
}
}

public get(key: string): any {
const store = this.storage.getStore();
return store ? store.get(key) : undefined;
}

public run(callback: () => void, initialValue?: Map<string, any>) {
this.storage.run(initialValue || new Map(), callback);
}
}
1 change: 1 addition & 0 deletions src/common/shared/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as JwtService } from './jwt.service';
export { default as ViewService } from './view.service';
export * from './mail';
export * from './logger.service';
export * from './async-localstorage.service';
1 change: 1 addition & 0 deletions src/common/shared/types/express.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { JwtPayload } from 'jsonwebtoken';
declare module 'express-serve-static-core' {
interface Request {
payload?: JwtPayload;
mongooseOptions?: Record<string, any>;
}
}
32 changes: 32 additions & 0 deletions src/core/engine/base/_models/_plugins/audit-trail.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Schema } from 'mongoose';
import { AsyncStorageService, logger } from '../../../../../common/shared';

const auditTrailPlugin = (schema: Schema) => {
schema.pre('save', function (next) {
const currentUserId =
AsyncStorageService.getInstance().get('currentUserId');

if (!currentUserId) {
logger.warn(
'Warning: currentUserId is undefined. Audit trail fields will not be set.',
);
}

if (this.isNew) {
this.set('createdBy', currentUserId || null);
} else {
this.set('updatedBy', currentUserId || null);
}
next();
});

schema.methods.softDelete = function () {
const currentUserId =
AsyncStorageService.getInstance().get('currentUserId');
this.deletedAt = new Date();
this.deletedBy = currentUserId || null;
return this.save();
};
};

export default auditTrailPlugin;
Loading

0 comments on commit 020ff65

Please sign in to comment.