From 5d79ffd055abb2ddbb70a2e1151b122b560cc98e Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Mon, 11 Aug 2025 22:54:31 +0700 Subject: [PATCH] :triangular_flag_on_post: add provision flow in oauth Create a flow where if the user logs in with a registered Google account, they are immediately authenticated, but if no account is found, create a new one. --- src/modules/auth/auth.types.ts | 11 +++--- .../controllers/googleCallback.controller.ts | 5 ++- .../services/http/googleCallback.service.ts | 28 ++++++++++++--- .../internal/OAuthUserProvision.service.ts | 35 +++++++++++++++++++ .../loginIfExistAndCreateIfNot.service.ts | 6 ---- .../read/findUserByProviderId.repository.ts | 22 ++++++++++++ .../services/internal/findUser.service.ts | 2 ++ src/modules/user/user.types.ts | 2 +- 8 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 src/modules/auth/services/internal/OAuthUserProvision.service.ts delete mode 100644 src/modules/auth/services/internal/loginIfExistAndCreateIfNot.service.ts create mode 100644 src/modules/user/repositories/read/findUserByProviderId.repository.ts diff --git a/src/modules/auth/auth.types.ts b/src/modules/auth/auth.types.ts index 20834be..4951ded 100644 --- a/src/modules/auth/auth.types.ts +++ b/src/modules/auth/auth.types.ts @@ -1,6 +1,9 @@ -export interface LoginIfExistAndCreateIfNot { - email: string; - username?: string; +export interface GoogleCallbackUserData { + sub: string; name: string; - provider: "Google" | "GitHub"; + given_name: string; + family_name: string; + picture: string; + email: string; + email_verified: boolean; } diff --git a/src/modules/auth/controllers/googleCallback.controller.ts b/src/modules/auth/controllers/googleCallback.controller.ts index 2b1bef4..eee3665 100644 --- a/src/modules/auth/controllers/googleCallback.controller.ts +++ b/src/modules/auth/controllers/googleCallback.controller.ts @@ -2,12 +2,15 @@ import { Context } from "elysia"; import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; import { mainErrorHandler } from "../../../helpers/error/handler"; import { googleCallbackService } from "../services/http/googleCallback.service"; +import { getUserHeaderInformation } from "../../../helpers/http/userHeader/getUserHeaderInformation"; export const googleCallbackController = async ( ctx: Context & { query: { code: string; state: string } } ) => { try { - const userData = await googleCallbackService(ctx.query); + const userHeaderInfo = getUserHeaderInformation(ctx); + + const userData = await googleCallbackService(ctx.query, userHeaderInfo); return returnWriteResponse( ctx.set, 200, diff --git a/src/modules/auth/services/http/googleCallback.service.ts b/src/modules/auth/services/http/googleCallback.service.ts index 972ecf4..c220578 100644 --- a/src/modules/auth/services/http/googleCallback.service.ts +++ b/src/modules/auth/services/http/googleCallback.service.ts @@ -2,11 +2,17 @@ import { AppError } from "../../../../helpers/error/instances/app"; import { googleProvider } from "../../providers/google.provider"; import { redis } from "../../../../utils/databases/redis/connection"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; +import { UserHeaderInformation } from "../../../../helpers/http/userHeader/getUserHeaderInformation/types"; +import { OAuthUserProvisionService } from "../internal/OAuthUserProvision.service"; +import { GoogleCallbackUserData } from "../../auth.types"; -export const googleCallbackService = async (query: { - state: string; - code: string; -}) => { +export const googleCallbackService = async ( + query: { + state: string; + code: string; + }, + userHeaderInfo: UserHeaderInformation +) => { try { // get code and state for validation from params and search for state in redis cache const state = query.state; @@ -36,7 +42,19 @@ export const googleCallbackService = async (query: { } ); - return await response.json(); + const userData = (await response.json()) as GoogleCallbackUserData; + + return await OAuthUserProvisionService( + { + providerName: "google", + openId: userData.sub, + email: userData.email, + name: userData.name, + avatar: userData.picture, + }, + userData, + userHeaderInfo + ); } catch (error) { ErrorForwarder(error, 500, "Authentication service error"); } diff --git a/src/modules/auth/services/internal/OAuthUserProvision.service.ts b/src/modules/auth/services/internal/OAuthUserProvision.service.ts new file mode 100644 index 0000000..763e6eb --- /dev/null +++ b/src/modules/auth/services/internal/OAuthUserProvision.service.ts @@ -0,0 +1,35 @@ +import { UserHeaderInformation } from "../../../../helpers/http/userHeader/getUserHeaderInformation/types"; +import { findUserService } from "../../../user/services/internal/findUser.service"; + +export const OAuthUserProvisionService = async ( + payload: { + providerName: string; + openId: string; + email: string; + username?: string; + name: string; + avatar?: string; + bio?: string; + }, + providerRawCallback: unknown, + userHeaderInfo: UserHeaderInformation +) => { + /** + * Create auth session if user already exist, + * create user account and give them auth session if not + * + * This is just example!! + */ + const providerId = `${payload.providerName}_${payload.openId}`; + const findUserResult = await findUserService({ + identifier: providerId, + queryTarget: "providerId", + options: { verbosity: "exists" }, + }); + + if (findUserResult) { + return "Already Created"; + } else { + return "Not Found"; + } +}; diff --git a/src/modules/auth/services/internal/loginIfExistAndCreateIfNot.service.ts b/src/modules/auth/services/internal/loginIfExistAndCreateIfNot.service.ts deleted file mode 100644 index 416dc0e..0000000 --- a/src/modules/auth/services/internal/loginIfExistAndCreateIfNot.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const loginIfExistAndCreateIfNotService = () => { - /** - * Create auth session if user already exist, - * create user account and give them auth session if not - */ -}; diff --git a/src/modules/user/repositories/read/findUserByProviderId.repository.ts b/src/modules/user/repositories/read/findUserByProviderId.repository.ts new file mode 100644 index 0000000..f29ef12 --- /dev/null +++ b/src/modules/user/repositories/read/findUserByProviderId.repository.ts @@ -0,0 +1,22 @@ +import { userModel } from "../../user.model"; +import { + getUserDataIncludeOptions, + getUserDataOptions, +} from "../../user.types"; + +export const findUserByProviderIdRepository = async ( + providerId: string, + include?: getUserDataOptions["include"] +) => { + return await userModel.findUnique({ + where: { + providerId, + }, + include: include + ? (Object.fromEntries(include.map((key) => [key, true])) as Record< + getUserDataIncludeOptions, + true + >) + : undefined, + }); +}; diff --git a/src/modules/user/services/internal/findUser.service.ts b/src/modules/user/services/internal/findUser.service.ts index b91265a..9c36906 100644 --- a/src/modules/user/services/internal/findUser.service.ts +++ b/src/modules/user/services/internal/findUser.service.ts @@ -4,12 +4,14 @@ import { AppError } from "../../../../helpers/error/instances/app"; import { findUserByIdRepository } from "../../repositories/read/findUserById.repository"; import { findUserByUsernameRepository } from "../../repositories/read/findUserByUsername.repository"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; +import { findUserByProviderIdRepository } from "../../repositories/read/findUserByProviderId.repository"; export const findUserService = async (payload: getUserDataService) => { try { // Define query target with the related repository const repositoryMap = { id: findUserByIdRepository, + providerId: findUserByProviderIdRepository, email: findUserByEmailRepository, username: findUserByUsernameRepository, } as const; diff --git a/src/modules/user/user.types.ts b/src/modules/user/user.types.ts index 59b7898..a01c195 100644 --- a/src/modules/user/user.types.ts +++ b/src/modules/user/user.types.ts @@ -1,6 +1,6 @@ export interface getUserDataService { identifier: string; - queryTarget: "id" | "email" | "username"; + queryTarget: "id" | "providerId" | "email" | "username"; options: getUserDataOptions; } export interface getUserDataOptions {