🚧 wip: rewrite reprovision logic to match new user schema

This commit is contained in:
2026-05-28 21:01:54 +07:00
parent 8cebc0cd20
commit 57d19d4302
13 changed files with 120 additions and 110 deletions

View File

@ -13,10 +13,12 @@ export const getUserHeaderInformation = (clientInfo: string): UserHeaderInformat
const clientInfoHeader = (JSON.parse(clientInfo) as ClientInfoHeader) ?? ("unknown" as string);
const userHeaderInformation = {
ip: clientInfoHeader.ip ?? "unknown",
deviceType: clientInfoHeader.deviceType ?? "unknown",
deviceOS: (clientInfoHeader.os ?? "unknown") + " " + (clientInfoHeader.osVersion ?? "unknown"),
browser: (clientInfoHeader.browser ?? "unknown") + " " + (clientInfoHeader.browserVersion ?? "unknown"),
ip: clientInfoHeader.ip,
deviceType: clientInfoHeader.deviceType ?? "desktop",
osType: clientInfoHeader.os ?? "unknown",
osVersion: clientInfoHeader.osVersion ?? "unknown",
browserName: clientInfoHeader.browser ?? "unknown",
browserVersion: clientInfoHeader.browserVersion ?? "unknown",
};
return userHeaderInformation;

View File

@ -1,6 +1,8 @@
export interface UserHeaderInformation {
ip: string;
ip?: string;
deviceType: string;
deviceOS: string;
browser: string;
osType: string;
osVersion: string;
browserName: string;
browserVersion: string;
}

View File

@ -0,0 +1,23 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { prisma } from "../../../../utils/databases/prisma/connection";
export const findAuthIdentityByEmailAndProviderRepository = async (email: string) => {
try {
return await prisma.user.findUnique({
where: {
email: email,
},
select: {
id: true,
oauth_accounts: {
select: {
provider_sub: true,
provider_name: true,
},
},
},
});
} catch (error) {
throw new AppError(500, "Error finding user by email", error);
}
};

View File

@ -12,14 +12,12 @@ export const googleCallbackService = async (
code: string;
callbackURI?: string;
},
userHeaderInfo: UserHeaderInformation
userHeaderInfo: UserHeaderInformation,
) => {
try {
// get code and state for validation from params and search for state in redis cache
const state = query.state;
const codeVerifier = await redis.get(
`${process.env.APP_NAME}:pkce:${state}`
);
const codeVerifier = await redis.get(`${process.env.APP_NAME}:pkce:${state}`);
// return error if the state for validation is not found in redis, and delete if found
if (!codeVerifier) throw new AppError(408, "Request timeout");
@ -27,21 +25,15 @@ export const googleCallbackService = async (
// create access token with the result of validating the authorization code that compares access code with validator state
const google = googleProvider(query.callbackURI);
const tokens = await google.validateAuthorizationCode(
query.code,
codeVerifier
);
const tokens = await google.validateAuthorizationCode(query.code, codeVerifier);
// get user data from Google using the access token that has been created.
const accessToken = tokens.accessToken();
const response = await fetch(
"https://openidconnect.googleapis.com/v1/userinfo",
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
const response = await fetch("https://openidconnect.googleapis.com/v1/userinfo", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
// parse the user data response
const userData = (await response.json()) as GoogleCallbackUserData;
@ -49,19 +41,17 @@ export const googleCallbackService = async (
// Provision or authenticate the user in the system
return await OAuthUserProvisionService(
{
provider: "google",
providerId: userData.sub,
providerToken: accessToken,
providerPayload: userData,
fullname: userData.name,
username: `gle_${userData.sub}`,
email: userData.email,
username: `goo_${userData.sub}`,
name: userData.name,
avatar: userData.picture,
password: Math.random()
.toString(36)
.slice(2, 16),
oauthProvider: {
providerName: "google",
sub: userData.sub,
token: accessToken,
},
},
userHeaderInfo
userHeaderInfo,
);
} catch (error) {
ErrorForwarder(error, 500, "Authentication service error");

View File

@ -1,39 +1,29 @@
import { User } from "@prisma/client";
import { UserHeaderInformation } from "../../../../helpers/http/userHeader/getUserHeaderInformation/types";
import { findUserService } from "../../../user/services/internal/findUser.service";
import { createUserSessionService } from "../../../userSession/services/createUserSession.service";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { createUserViaOauth } from "../../../user/user.types";
import { createUserService } from "../../../user/services/internal/createUser.service";
import { AppError } from "../../../../helpers/error/instances/app";
import { findAuthIdentityByEmailAndProviderRepository } from "../../repositories/READ/findAuthIdentityByEmailAndProvider.repository";
export const OAuthUserProvisionService = async (
payload: createUserViaOauth,
userHeaderInfo: UserHeaderInformation
) => {
export const OAuthUserProvisionService = async (payload: createUserViaOauth, userHeaderInfo: UserHeaderInformation) => {
try {
const providerId = payload.providerId;
const findUserResult = (await findUserService({
identifier: providerId,
queryTarget: "providerId",
options: { verbosity: "full" },
})) as User;
if (findUserResult) {
return await createUserSessionService(findUserResult.id, userHeaderInfo);
} else {
const findUserByEmailOnly = await findUserService({
identifier: payload.email,
queryTarget: "email",
options: { verbosity: "exist" },
});
if (findUserByEmailOnly)
throw new AppError(409, "Email already in use with another account");
const checkExistingUser = await findAuthIdentityByEmailAndProviderRepository(payload.email);
if (
checkExistingUser &&
checkExistingUser.oauth_accounts.some((account) => account.provider_sub === payload.oauthProvider.sub)
) {
// User already exists with this OAuth provider
return await createUserSessionService(checkExistingUser.id, userHeaderInfo);
} else if (!checkExistingUser) {
// No user with this email, create new user
const createdUser = await createUserService(payload);
return await createUserSessionService(createdUser.id, userHeaderInfo);
}
// User exists with this email but not with this OAuth provider
throw new AppError(409, "Email already in use with another account");
} catch (error) {
ErrorForwarder(error);
}

View File

@ -3,18 +3,14 @@ import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { userModel } from "../../user.model";
import { createUserViaRegisterInput } from "../../user.types";
export const createUserViaRegisterRepository = async (
payload: createUserViaRegisterInput,
) => {
export const createUserViaRegisterRepository = async (payload: createUserViaRegisterInput) => {
try {
return await userModel.create({
data: {
...payload,
id: generateUUIDv7(),
preference: {
create: {
id: generateUUIDv7(),
},
preferences: {
create: {},
},
},
});

View File

@ -1,21 +1,17 @@
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { hashPassword } from "../../../../helpers/security/password/hash";
import { createUserViaRegisterRepository } from "../../repositories/create/createUserViaRegister.repository";
import {
createUserViaOauth,
createUserViaRegisterInput,
} from "../../user.types";
import { createUserViaOauth, createUserViaRegisterInput } from "../../user.types";
export const createUserService = async (
payload: createUserViaRegisterInput | createUserViaOauth
) => {
export const createUserService = async (payload: createUserViaRegisterInput) => {
try {
const hashedPassword = await hashPassword(payload.password);
if ("password" in payload && payload.password)
return await createUserViaRegisterRepository({
...payload,
password: await hashPassword(payload.password),
} as createUserViaRegisterInput);
return await createUserViaRegisterRepository({
...payload,
password: hashedPassword,
});
return await createUserViaRegisterRepository(payload as Omit<createUserViaOauth, "oauthProvider">);
} catch (error) {
ErrorForwarder(error);
}

View File

@ -10,20 +10,20 @@ export interface getUserDataOptions {
export type getUserDataIncludeOptions = "preference" | "roles";
export interface createUserViaRegisterInput {
name: string;
fullname: string;
username: string;
email: string;
password: string;
}
export interface createUserViaOauth {
provider: string;
providerId: string;
providerToken?: string;
providerPayload?: unknown;
email: string;
username: string;
name: string;
password?: string;
avatar?: string;
bio?: string;
password: string;
datebirth?: Date;
}
export interface createUserViaOauth extends createUserViaRegisterInput {
oauthProvider: {
providerName: string;
sub: string;
token: string;
refreshToken?: string;
expiresAt?: Date;
};
}

View File

@ -5,24 +5,21 @@ import { createUserSessionRepository } from "../repositories/createUserSession.r
import { jwtEncode } from "../../../helpers/http/jwt/encode";
import { createUserSessionInRedisService } from "./internal/createUserSessionInRedis.service";
export const createUserSessionService = async (
userId: string,
userHeaderInfo: UserHeaderInformation,
) => {
export const createUserSessionService = async (userId: string, userHeaderInfo: UserHeaderInformation) => {
try {
// set the date when the token will expire
const generateTokenExpirationDate =
Date.now() + Number(process.env.SESSION_EXPIRE) * 1000;
const generateTokenExpirationDate = Date.now() + Number(process.env.SESSION_EXPIRE) * 1000;
// construct all data to fit the user session input query
const constructData = {
userId,
isAuthenticated: true,
deviceType: userHeaderInfo.deviceType,
deviceOs: userHeaderInfo.deviceOS,
deviceIp: userHeaderInfo.ip,
browser: userHeaderInfo.browser,
validUntil: new Date(generateTokenExpirationDate),
user_id: userId,
device_type: userHeaderInfo.deviceType,
os_type: userHeaderInfo.osType,
os_version: userHeaderInfo.osVersion,
browser_name: userHeaderInfo.browserName,
browser_version: userHeaderInfo.browserVersion,
ip_login: userHeaderInfo.ip,
valid_until: new Date(generateTokenExpirationDate),
} as Prisma.UserSessionUncheckedCreateInput;
// insert user session into database