🚩 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.
This commit is contained in:
Rafi Arrafif
2025-08-11 22:54:31 +07:00
parent 7ce0e44389
commit 5d79ffd055
8 changed files with 94 additions and 17 deletions

View File

@ -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;
}

View File

@ -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,

View File

@ -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");
}

View File

@ -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";
}
};

View File

@ -1,6 +0,0 @@
export const loginIfExistAndCreateIfNotService = () => {
/**
* Create auth session if user already exist,
* create user account and give them auth session if not
*/
};

View File

@ -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,
});
};

View File

@ -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;

View File

@ -1,6 +1,6 @@
export interface getUserDataService {
identifier: string;
queryTarget: "id" | "email" | "username";
queryTarget: "id" | "providerId" | "email" | "username";
options: getUserDataOptions;
}
export interface getUserDataOptions {