diff --git a/src/modules/auth/controller/logout.controller.ts b/src/modules/auth/controller/logout.controller.ts new file mode 100644 index 0000000..1fab37a --- /dev/null +++ b/src/modules/auth/controller/logout.controller.ts @@ -0,0 +1,31 @@ +import { Context } from "elysia"; +import { getCookie } from "../../../helpers/http/userHeader/cookies/getCookies"; +import { clearCookies } from "../../../helpers/http/userHeader/cookies/clearCookies"; +import { mainErrorHandler } from "../../../helpers/error/handler"; +import { COOKIE_KEYS } from "../../../constants/cookie.keys"; +import { + returnErrorResponse, + returnWriteResponse, +} from "../../../helpers/callback/httpResponse"; +import { logoutService } from "../services/logout.service"; + +export const logoutController = async (ctx: Context) => { + try { + const userCookie = getCookie(ctx); + if (!userCookie || !userCookie.auth_token) { + return returnErrorResponse(ctx.set, 401, "You're not logged in yet"); + } + + const clearSession = logoutService(userCookie.auth_token); + + clearCookies(ctx.set, [COOKIE_KEYS.AUTH]); + return returnWriteResponse( + ctx.set, + 200, + "Successfully logged out", + clearSession + ); + } catch (error) { + return mainErrorHandler(ctx.set, error); + } +}; diff --git a/src/modules/auth/index.ts b/src/modules/auth/index.ts index d2140a3..fe4425b 100644 --- a/src/modules/auth/index.ts +++ b/src/modules/auth/index.ts @@ -2,9 +2,11 @@ import Elysia from "elysia"; import { loginWithPassword } from "./controller/loginWithPassword.controller"; import { authMiddleware } from "../../middleware/auth.middleware"; import { authVerification } from "./controller/authVerification.controller"; +import { logoutController } from "./controller/logout.controller"; export const authModule = new Elysia({ prefix: "/auth" }) .post("/legacy", loginWithPassword) .post("/verification", authVerification, { beforeHandle: authMiddleware, - }); + }) + .post("/logout", logoutController); diff --git a/src/modules/auth/services/authVerification.service.ts b/src/modules/auth/services/authVerification.service.ts index bdcd4ea..c630e6b 100644 --- a/src/modules/auth/services/authVerification.service.ts +++ b/src/modules/auth/services/authVerification.service.ts @@ -1,3 +1,4 @@ +import { Prisma } from "@prisma/client"; import { AppError } from "../../../helpers/error/instances/app"; import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; import { jwtDecode } from "../../../helpers/http/jwt/decode"; diff --git a/src/modules/auth/services/loginFromSystem.service.ts b/src/modules/auth/services/loginFromSystem.service.ts new file mode 100644 index 0000000..9b5f803 --- /dev/null +++ b/src/modules/auth/services/loginFromSystem.service.ts @@ -0,0 +1 @@ +export const loginFromSystemService = (userId: string) => {}; diff --git a/src/modules/auth/services/logout.service.ts b/src/modules/auth/services/logout.service.ts new file mode 100644 index 0000000..2a2c33c --- /dev/null +++ b/src/modules/auth/services/logout.service.ts @@ -0,0 +1,15 @@ +import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; +import { jwtDecode } from "../../../helpers/http/jwt/decode"; +import { deleteUserSessionInCacheAndDBService } from "../../userSession/services/deleteUserSessionInCacheAndDB.service"; + +export const logoutService = async (userCookie: string) => { + try { + const jwtToken = jwtDecode(userCookie); + const deleteUserSessionInCacheAndDB = + deleteUserSessionInCacheAndDBService(jwtToken); + + return deleteUserSessionInCacheAndDB; + } catch (error) { + ErrorForwarder(error, 500, "Logout service had encountered error"); + } +}; diff --git a/src/modules/user/controller/createUser.controller.ts b/src/modules/user/controller/createUser.controller.ts index 3c4dead..aa2d7ee 100644 --- a/src/modules/user/controller/createUser.controller.ts +++ b/src/modules/user/controller/createUser.controller.ts @@ -7,6 +7,7 @@ import { Context } from "elysia"; import { createUserService } from "../services/createUser.service"; import { mainErrorHandler } from "../../../helpers/error/handler"; import { createUserSchema } from "../schemas/createUser.schema"; +import { getCookie } from "../../../helpers/http/userHeader/cookies/getCookies"; /** * @function createUser @@ -30,6 +31,19 @@ import { createUserSchema } from "../schemas/createUser.schema"; export const createUserController = async ( ctx: Context & { body: Prisma.UserCreateInput } ) => { + // Check if the user is already logged in by checking the auth token in cookies. If the user is logged in, return an error response + try { + const cookie = getCookie(ctx); + if (cookie && cookie.auth_token) + return returnErrorResponse( + ctx.set, + 401, + "You are already logged in. Please log out first if you want to create a new account." + ); + } catch (_) { + // Pass + } + // Validate the user input using a validation schema const { error } = createUserSchema.validate(ctx.body); if (error) diff --git a/src/modules/user/services/editUser.service.ts b/src/modules/user/services/editUser.service.ts index 32bb070..a8284b0 100644 --- a/src/modules/user/services/editUser.service.ts +++ b/src/modules/user/services/editUser.service.ts @@ -25,6 +25,7 @@ export const editUserService = async ( "The username or email has already taken by another user." ); + // Prepare the fields to update, only include fields that are provided in the payload const fieldsToUpdate: Partial = { ...(payload.username && payload.username !== jwtSession.user.username ? { username: payload.username } @@ -53,6 +54,7 @@ export const editUserService = async ( : {}), }; + // Update the user in the database, use username from the JWT session to find the user const updateUser = await updateUserRepo( jwtSession.user.username, fieldsToUpdate diff --git a/src/modules/userSession/repositories/deleteUserSessionFromCache.repository.ts b/src/modules/userSession/repositories/deleteUserSessionFromCache.repository.ts new file mode 100644 index 0000000..d75aaf4 --- /dev/null +++ b/src/modules/userSession/repositories/deleteUserSessionFromCache.repository.ts @@ -0,0 +1,16 @@ +import { AppError } from "../../../helpers/error/instances/app"; +import { redis } from "../../../utils/databases/redis/connection"; + +export const deleteUserSessionFromCacheRepo = async ( + userId: string, + sessionId: string +) => { + try { + const deleteUserSessionFromCache = redis.del( + `${process.env.APP_NAME}:users:${userId}:sessions:${sessionId}` + ); + return deleteUserSessionFromCache; + } catch (error) { + throw new AppError(500, "Error while remove data from cache", error); + } +}; diff --git a/src/modules/userSession/repositories/deleteUserSessionFromDB.repository.ts b/src/modules/userSession/repositories/deleteUserSessionFromDB.repository.ts new file mode 100644 index 0000000..07b707e --- /dev/null +++ b/src/modules/userSession/repositories/deleteUserSessionFromDB.repository.ts @@ -0,0 +1,19 @@ +import { AppError } from "../../../helpers/error/instances/app"; +import { prisma } from "../../../utils/databases/prisma/connection"; + +export const deleteUserSessionFromDBRepo = async (sessionId: string) => { + try { + const deleteUserSessionFromCacheDB = await prisma.userSession.update({ + where: { + id: sessionId, + }, + data: { + deletedAt: new Date(), + }, + }); + + return deleteUserSessionFromCacheDB; + } catch (error) { + throw new AppError(500, "Error while remove delete from database", error); + } +}; diff --git a/src/modules/userSession/repositories/findUniqueUserSessionInDB.repository.ts b/src/modules/userSession/repositories/findUniqueUserSessionInDB.repository.ts index fe29363..57b296a 100644 --- a/src/modules/userSession/repositories/findUniqueUserSessionInDB.repository.ts +++ b/src/modules/userSession/repositories/findUniqueUserSessionInDB.repository.ts @@ -19,7 +19,6 @@ export const findUniqueUserSessionInDBRepo = async (identifier: string) => { }, }, omit: { - deletedAt: true, updatedAt: true, }, }); diff --git a/src/modules/userSession/repositories/insertUserSessionToDB.repository.ts b/src/modules/userSession/repositories/insertUserSessionToDB.repository.ts index 0f042bc..c5cd787 100644 --- a/src/modules/userSession/repositories/insertUserSessionToDB.repository.ts +++ b/src/modules/userSession/repositories/insertUserSessionToDB.repository.ts @@ -20,7 +20,6 @@ export const createUserSessionRepo = async ( }, omit: { lastOnline: true, - deletedAt: true, createdAt: true, updatedAt: true, }, diff --git a/src/modules/userSession/repositories/storeUserSessionToCache.repository.ts b/src/modules/userSession/repositories/storeUserSessionToCache.repository.ts index 753889a..866a72d 100644 --- a/src/modules/userSession/repositories/storeUserSessionToCache.repository.ts +++ b/src/modules/userSession/repositories/storeUserSessionToCache.repository.ts @@ -8,7 +8,7 @@ export const storeUserSessionToCacheRepo = async ( ) => { try { await redis.set( - `${process.env.app_name}:users:${userSession.userId}:sessions:${userSession.id}`, + `${process.env.APP_NAME}:users:${userSession.userId}:sessions:${userSession.id}`, String(userSession.validUntil), "EX", timeExpires diff --git a/src/modules/userSession/services/checkUserSessionInCache.service.ts b/src/modules/userSession/services/checkUserSessionInCache.service.ts index d005b5b..ceb61eb 100644 --- a/src/modules/userSession/services/checkUserSessionInCache.service.ts +++ b/src/modules/userSession/services/checkUserSessionInCache.service.ts @@ -7,7 +7,7 @@ export const checkUserSessionInCacheService = async ( ) => { try { // Construct the Redis key name using the userId and sessionId - const redisKeyName = `${process.env.app_name}:users:${userId}:sessions:${sessionId}`; + const redisKeyName = `${process.env.APP_NAME}:users:${userId}:sessions:${sessionId}`; // Check if the user session exists in Redis const userSessionInRedis = await checkUserSessionInCacheRepo(redisKeyName); diff --git a/src/modules/userSession/services/deleteUserSessionInCacheAndDB.service.ts b/src/modules/userSession/services/deleteUserSessionInCacheAndDB.service.ts new file mode 100644 index 0000000..df968f4 --- /dev/null +++ b/src/modules/userSession/services/deleteUserSessionInCacheAndDB.service.ts @@ -0,0 +1,25 @@ +import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; +import { JWTAuthToken } from "../../../helpers/http/jwt/decode/types"; +import { deleteUserSessionFromCacheRepo } from "../repositories/deleteUserSessionFromCache.repository"; +import { deleteUserSessionFromDBRepo } from "../repositories/deleteUserSessionFromDB.repository"; + +export const deleteUserSessionInCacheAndDBService = async ( + jwtToken: JWTAuthToken +) => { + try { + const userId = jwtToken.userId; + const sessionId = jwtToken.id; + + await deleteUserSessionFromCacheRepo(userId, sessionId); + const deleteUserSessionFromDB = await deleteUserSessionFromDBRepo( + sessionId + ); + return deleteUserSessionFromDB; + } catch (error) { + ErrorForwarder( + error, + 500, + "Delete user session service had encountered error" + ); + } +}; diff --git a/src/modules/userSession/services/getUserSessionFromDB.service.ts b/src/modules/userSession/services/getUserSessionFromDB.service.ts index 1c4837e..42787ae 100644 --- a/src/modules/userSession/services/getUserSessionFromDB.service.ts +++ b/src/modules/userSession/services/getUserSessionFromDB.service.ts @@ -10,6 +10,7 @@ export const getUserSessionFromDBService = async (identifier: string) => { if ( !userSession || !userSession.isAuthenticated || + userSession.deletedAt || new Date(userSession.validUntil) < new Date() ) return false;