Skip to content

Commit

Permalink
refactor: to use AuthenticationMiddleware instead of decode token in …
Browse files Browse the repository at this point in the history
…create chat
  • Loading branch information
JoseAlbDR committed Apr 10, 2024
1 parent 3fa45e1 commit 4cc9020
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 30 deletions.
1 change: 1 addition & 0 deletions src/config/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { get } from 'env-var';
export const envs = {
PORT: get('PORT').required().asPortNumber(),
OPENAI_API_KEY: get('OPENAI_API_KEY').required().asString(),
NODE_ENV: get('NODE_ENV').required().asString(),

SUPABASE_URL: get('SUPABASE_URL').required().asString(),
SUPABASE_PRIVATE_KEY: get('SUPABASE_PRIVATE_KEY').required().asString(),
Expand Down
12 changes: 12 additions & 0 deletions src/domain/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PayloadUser } from '../interfaces/payload-user.interface';

export {};

declare global {
namespace Express {
interface Request {
user: PayloadUser;
}
}
}
req.user;
16 changes: 10 additions & 6 deletions src/presentation/chatbot/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ export class ChatbotController {
* @param res The response object.
*/
createChat = async (req: Request, res: Response) => {
const { token } = req.params;
const id = req.user.id;

await this.chatbotService.createChat(token);
await this.chatbotService.createChat(id!);
res.status(200).json({ message: 'Chat created successfully' });
};

Expand All @@ -62,8 +62,10 @@ export class ChatbotController {
* @param _req The request object.
* @param res The response object.
*/
getChatHistory = async (_req: Request, res: Response) => {
const history = await this.chatbotService.getChatHistory();
getChatHistory = async (req: Request, res: Response) => {
const id = req.user.id;

const history = await this.chatbotService.getChatHistory(id!);
res.status(200).json(history);
};

Expand All @@ -72,8 +74,10 @@ export class ChatbotController {
* @param _req The request object.
* @param res The response object.
*/
deleteChatHistory = async (_req: Request, res: Response) => {
await this.chatbotService.deleteChatHistory();
deleteChatHistory = async (req: Request, res: Response) => {
const id = req.user.id;

await this.chatbotService.deleteChatHistory(id!);
res.status(200).json({ message: 'Chat history deleted successfully' });
};
}
7 changes: 5 additions & 2 deletions src/presentation/chatbot/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,26 @@ import { ChatbotController } from './controller';
import { envs } from '../../config/envs';
import { MemoryService } from '../memory/service';
import { JWTAdapter } from '../../config/jwt.adapter';
import { AuthMiddleware } from '../middlewares/auth-middleware';

export class ChatbotRoutes {
static get routes() {
const router = Router();
const jwt = new JWTAdapter(envs.JWT_SEED);
const authMiddleware = new AuthMiddleware(jwt);
const memoryService = new MemoryService(envs.MONGO_DB_URL);
const chatbotService = new ChatbotService(
{ maxTokens: 500, openAIApiKey: envs.OPENAI_API_KEY, temperature: 0.7 },
{
supabaseKey: envs.SUPABASE_PRIVATE_KEY,
supabaseUrl: envs.SUPABASE_URL,
},
memoryService,
jwt
memoryService
);
const chatbotController = new ChatbotController(chatbotService);

router.use(authMiddleware.authenticateUser);

router.delete('/chat-history/', chatbotController.deleteChatHistory);
router.get('/chat-history', chatbotController.getChatHistory);
router.post('/create-chat/:token', chatbotController.createChat);
Expand Down
40 changes: 20 additions & 20 deletions src/presentation/chatbot/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export class ChatbotService {
private readonly embeddings = new OpenAIEmbeddings();
private readonly client: SupabaseClient;
private agentExecutor?: AgentExecutor;
private memory?: BufferMemory;
private chat_history: BaseMessage[] = [];

/**
Expand All @@ -47,8 +46,7 @@ export class ChatbotService {
constructor(
private readonly openAIOptions: OpenAIOptions,
private readonly supabaseOptions: SupabaseOptions,
private readonly memoryService: MemoryService,
private readonly jwt: JWTAdapter
private readonly memoryService: MemoryService
) {
const { maxTokens, openAIApiKey, temperature } = this.openAIOptions;
try {
Expand Down Expand Up @@ -80,25 +78,21 @@ export class ChatbotService {
* Creates a new chat session.
* @param token JWT token for authentication.
*/
async createChat(token: string) {
async createChat(userId: string) {
try {
const payload = this.jwt.validateToken(token);

if (!payload) throw new UnauthenticatedError('Invalid JWT token');

const {
user: { name: username },
} = payload;

const prompt = this.createPrompt('adoptaunpeludo.com');

const retrieverTool = await this.createRetrieverTool();

this.memory = await this.memoryService.createMemory(username!);
const memory = await this.memoryService.createMemory(userId);

const tools = [retrieverTool];

this.agentExecutor = await this.createAgentExecutor(tools, prompt);
this.agentExecutor = await this.createAgentExecutor(
tools,
prompt,
memory
);
} catch (error) {
console.log(error);
throw new InternalServerError('Error creating chat, check logs');
Expand Down Expand Up @@ -168,7 +162,11 @@ export class ChatbotService {
* @returns The agent executor instance.
* @throws Throws an error if there's an issue creating the agent executor.
*/
private async createAgentExecutor(tools: any, prompt: ChatPromptTemplate) {
private async createAgentExecutor(
tools: any,
prompt: ChatPromptTemplate,
memory: BufferMemory
) {
try {
const agent = await createOpenAIFunctionsAgent({
llm: this.model,
Expand All @@ -179,7 +177,7 @@ export class ChatbotService {
const agentExecutor = new AgentExecutor({
agent,
tools,
memory: this.memory,
memory,
});

return agentExecutor;
Expand Down Expand Up @@ -216,9 +214,10 @@ export class ChatbotService {
* Retrieves the chat history.
* @returns The chat history.
*/
public async getChatHistory() {
public async getChatHistory(userId: string) {
try {
this.chat_history = await this.memory!.chatHistory.getMessages();
const memory = await this.memoryService.createMemory(userId);
this.chat_history = await memory.chatHistory.getMessages();

return ChatHistoryEntity.fromObject(this.chat_history);
} catch (error) {
Expand All @@ -230,9 +229,10 @@ export class ChatbotService {
/**
* Deletes the chat history.
*/
public async deleteChatHistory() {
public async deleteChatHistory(userId: string) {
try {
await this.memory?.chatHistory.clear();
const memory = await this.memoryService.createMemory(userId);
await memory.chatHistory.clear();
} catch (error) {
console.log(error);
throw new InternalServerError('Error clearing chat history, check logs');
Expand Down
4 changes: 2 additions & 2 deletions src/presentation/memory/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class MemoryService {
return await this.connectToCollection();
}

async createMemory(username: string) {
async createMemory(userId: string) {
const collection = await this.getCollection();

try {
Expand All @@ -33,7 +33,7 @@ export class MemoryService {
outputKey: 'output',
chatHistory: new MongoDBChatMessageHistory({
collection,
sessionId: username,
sessionId: userId,
}),
});

Expand Down
83 changes: 83 additions & 0 deletions src/presentation/middlewares/auth-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { NextFunction, Request, Response } from 'express';

import { AttachCookiesToResponse } from '../../utils/response-cookies';
import { UnauthenticatedError, UnauthorizedError } from '../../domain/errors';
import { JWTAdapter } from '../../config/jwt.adapter';
import { UserRoles } from '../../domain/interfaces/payload-user.interface';

/**
* Middleware class for authentication and authorization.
*/
export class AuthMiddleware {
/**
* Constructs an instance of AuthMiddleware.
* @param jwt - Instance of JWTAdapter for handling JSON Web Tokens.
*/
constructor(private readonly jwt: JWTAdapter) {}

/**
* Middleware for authenticating user requests.
*/
public authenticateUser = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { refreshToken, accessToken } = req.signedCookies;

// Check if refresh token and access token are present
if (!refreshToken && !accessToken)
throw new UnauthenticatedError('Please first login');

if (accessToken) {
// Validate access token
const payload = this.jwt.validateToken(accessToken);
if (!payload) throw new UnauthorizedError('Invalid token validation');
req.user = payload.user;
const wsToken = this.jwt.generateToken({ user: payload.user }, '1d');
req.user.wsToken = wsToken;
return next();
}

// Validate refresh token
const payload = this.jwt.validateToken(refreshToken);
if (!payload) throw new UnauthorizedError('Invalid token validation');

const { user } = payload;

// Generate new tokens
const accessTokenJWT = this.jwt.generateToken({ user }, '15m');
const refreshTokenJWT = this.jwt.generateToken(
{ user, refreshToken },
'1d'
);

// Attach new tokens to response cookies
AttachCookiesToResponse.attach({
res,
accessToken: accessTokenJWT!,
refreshToken: refreshTokenJWT!,
});

req.user = user;
req.user.wsToken = accessTokenJWT;
next();
};

/**
* Middleware for authorizing user permissions based on roles.
* @param roles - Roles allowed to access the resource.
*/
public authorizePermissions = (...roles: UserRoles[]) => {
return (req: Request, res: Response, next: NextFunction) => {
const { role } = req.user;

if (role === 'admin') return next();

if (!roles.includes(role!))
throw new UnauthorizedError('Unauthorized to access this resource');

next();
};
};
}
39 changes: 39 additions & 0 deletions src/utils/response-cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Response } from 'express';
import { envs } from '../config/envs';

interface Options {
res: Response;
accessToken: string;
refreshToken: string;
}

/**
* Utility class for attaching cookies to the response.
*/
export class AttachCookiesToResponse {
/**
* Method to attach access and refresh tokens as cookies to the response.
* @param options - Object containing response object and token values.
*/
static attach({ res, accessToken, refreshToken }: Options) {
// Define cookie expiration times
const oneMinute = 1000 * 60;
const oneDay = 1000 * 60 * 60 * 24;

// Attach access token cookie to the response
res.cookie('accessToken', accessToken, {
httpOnly: true,
secure: envs.NODE_ENV === 'production',
signed: true,
expires: new Date(Date.now() + oneMinute),
});

// Attach refresh token cookie to the response
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
expires: new Date(Date.now() + oneDay),
secure: envs.NODE_ENV === 'production',
signed: true,
});
}
}

0 comments on commit 4cc9020

Please sign in to comment.