From 08125dcb118bafe12cb4dc3d8fb7e76df6dddd3d Mon Sep 17 00:00:00 2001 From: Vivy Bot Date: Tue, 10 Feb 2026 23:12:04 +0700 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20logout=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/helpers/http/jwt/decode/index.ts | 5 ++--- .../auth/controllers/logout.controller.ts | 19 +++++++++++++++++++ src/modules/auth/index.ts | 4 +++- .../auth/services/http/logout.service.ts | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 src/modules/auth/controllers/logout.controller.ts create mode 100644 src/modules/auth/services/http/logout.service.ts diff --git a/src/helpers/http/jwt/decode/index.ts b/src/helpers/http/jwt/decode/index.ts index aaf5fe5..caed720 100644 --- a/src/helpers/http/jwt/decode/index.ts +++ b/src/helpers/http/jwt/decode/index.ts @@ -1,15 +1,14 @@ import jwt from "jsonwebtoken"; -import { JWTSessionPayload } from "../../../../modules/auth/auth.types"; +import { JWTAuthToken } from "./types"; import { AppError } from "../../../error/instances/app"; export const jwtDecode = (payload: string) => { - // return payload; if (!payload) throw new AppError(401, "Unauthorized"); const JWTKey = process.env.JWT_SECRET!; try { const decodedPayload = jwt.verify(payload, JWTKey); - return decodedPayload as JWTSessionPayload; + return decodedPayload as JWTAuthToken; } catch (error) { throw new AppError(401, "Invalid or expired token", error); } diff --git a/src/modules/auth/controllers/logout.controller.ts b/src/modules/auth/controllers/logout.controller.ts new file mode 100644 index 0000000..4d8f23f --- /dev/null +++ b/src/modules/auth/controllers/logout.controller.ts @@ -0,0 +1,19 @@ +import { Context } from "elysia"; +import { mainErrorHandler } from "../../../helpers/error/handler"; +import { logoutService } from "../services/http/logout.service"; +import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; + +export const logoutController = async (ctx: Context) => { + try { + const jwtToken = ctx.cookie.auth_token?.value; + const serviceResponse = await logoutService(jwtToken); + return returnWriteResponse( + ctx.set, + 200, + "Logout successful", + serviceResponse, + ); + } catch (error) { + return mainErrorHandler(ctx.set, error); + } +}; diff --git a/src/modules/auth/index.ts b/src/modules/auth/index.ts index b6abb6d..bf0db41 100644 --- a/src/modules/auth/index.ts +++ b/src/modules/auth/index.ts @@ -6,6 +6,7 @@ import { googleCallbackController } from "./controllers/googleCallback.controlle import { getOauthProvidersController } from "./controllers/getOauthProviders.controller"; import { getCallbackProviderUrlController } from "./controllers/getCallbackProviderUrl.controller"; import { tokenValidationController } from "./controllers/tokenValidation.controller"; +import { logoutController } from "./controllers/logout.controller"; export const authModule = new Elysia({ prefix: "/auth" }) .post("/token/validate", tokenValidationController) @@ -14,4 +15,5 @@ export const authModule = new Elysia({ prefix: "/auth" }) .get("/github", githubRequestController) .get("/github/callback", githubCallbackController) .get("/google", googleRequestController) - .get("/google/callback", googleCallbackController); + .get("/google/callback", googleCallbackController) + .post("/logout", logoutController); diff --git a/src/modules/auth/services/http/logout.service.ts b/src/modules/auth/services/http/logout.service.ts new file mode 100644 index 0000000..0662960 --- /dev/null +++ b/src/modules/auth/services/http/logout.service.ts @@ -0,0 +1,14 @@ +import { AppError } from "../../../../helpers/error/instances/app"; +import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; +import { jwtDecode } from "../../../../helpers/http/jwt/decode"; + +export const logoutService = async (jwtToken?: any) => { + try { + if (!jwtToken) throw new AppError(403, "No auth token provided"); + + const jwtPayload = jwtDecode(jwtToken); + return jwtPayload; + } catch (error) { + ErrorForwarder(error); + } +}; -- 2.49.0 From 588ac49e01dfe7b249398909870c25b894638cf5 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Fri, 13 Feb 2026 19:38:16 +0700 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=9A=A8=20fix:=20update=20TypeScript?= =?UTF-8?q?=20type=20for=20decodeJWT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/helpers/http/jwt/decode/types.ts | 54 ++++++---------------------- 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/src/helpers/http/jwt/decode/types.ts b/src/helpers/http/jwt/decode/types.ts index 0320410..1b2950b 100644 --- a/src/helpers/http/jwt/decode/types.ts +++ b/src/helpers/http/jwt/decode/types.ts @@ -1,16 +1,7 @@ export interface JWTAuthToken { id: string; isAuthenticated: boolean; - userId: string; - deviceType: string; - deviceOs: string; - deviceIp: string; - isOnline: boolean; - lastOnline: Date; validUntil: Date; - deletedAt: null; - createdAt: Date; - updatedAt: Date; user: User; iat: number; exp: number; @@ -19,44 +10,21 @@ export interface JWTAuthToken { interface User { id: string; name: string; - username: string; email: string; + username: string; + avatar: string; birthDate: null; - gender: null; - phoneCC: null; - phoneNumber: null; bioProfile: null; - profilePicture: null; - commentPicture: null; - preferenceId: null; - verifiedAt: null; - disabledAt: null; - deletedAt: null; - createdAt: Date; - updatedAt: Date; - roles: Role[]; + preference: Preference; } -interface Role { +interface Preference { id: string; - name: string; - primaryColor: string; - secondaryColor: string; - pictureImage: string; - badgeImage: null; - isSuperadmin: boolean; - canEditMedia: boolean; - canManageMedia: boolean; - canEditEpisodes: boolean; - canManageEpisodes: boolean; - canEditComment: boolean; - canManageComment: boolean; - canEditUser: boolean; - canManageUser: boolean; - canEditSystem: boolean; - canManageSystem: boolean; - createdBy: string; - deletedAt: null; - createdAt: Date; - updatedAt: Date; + userId: string; + langPreference: null; + adultFiltering: string; + adultAlert: string; + videoQuality: string; + serviceDefaultId: null; + hideContries: any[]; } -- 2.49.0 From 42aa7ed8d35c93ff75c67a71dc550fe5c28c0953 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Fri, 13 Feb 2026 19:46:44 +0700 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20logout=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/helpers/http/jwt/decode/example.json | 0 .../auth/services/http/logout.service.ts | 9 ++++++++- .../deleteUserSession.repository.ts | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/helpers/http/jwt/decode/example.json create mode 100644 src/modules/userSession/repositories/deleteUserSession.repository.ts diff --git a/src/helpers/http/jwt/decode/example.json b/src/helpers/http/jwt/decode/example.json new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/auth/services/http/logout.service.ts b/src/modules/auth/services/http/logout.service.ts index 0662960..40ade6a 100644 --- a/src/modules/auth/services/http/logout.service.ts +++ b/src/modules/auth/services/http/logout.service.ts @@ -1,13 +1,20 @@ import { AppError } from "../../../../helpers/error/instances/app"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { jwtDecode } from "../../../../helpers/http/jwt/decode"; +import { redis } from "../../../../utils/databases/redis/connection"; +import { deleteUserSessionRepository } from "../../../userSession/repositories/deleteUserSession.repository"; export const logoutService = async (jwtToken?: any) => { try { if (!jwtToken) throw new AppError(403, "No auth token provided"); const jwtPayload = jwtDecode(jwtToken); - return jwtPayload; + + await redis.del( + `${process.env.APP_NAME}:users:${jwtPayload.user.id}:sessions:${jwtPayload.id}`, + ); + + return deleteUserSessionRepository(jwtPayload.id); } catch (error) { ErrorForwarder(error); } diff --git a/src/modules/userSession/repositories/deleteUserSession.repository.ts b/src/modules/userSession/repositories/deleteUserSession.repository.ts new file mode 100644 index 0000000..def23ad --- /dev/null +++ b/src/modules/userSession/repositories/deleteUserSession.repository.ts @@ -0,0 +1,18 @@ +import { AppError } from "../../../helpers/error/instances/app"; +import { prisma } from "../../../utils/databases/prisma/connection"; + +export const deleteUserSessionRepository = async (sessionId: string) => { + try { + return await prisma.userSession.update({ + where: { + id: sessionId, + }, + data: { + isAuthenticated: false, + deletedAt: new Date(), + }, + }); + } catch (error) { + throw new AppError(500, "Failed to delete user session", error); + } +}; -- 2.49.0