feature/adapt-to-new-database #34
@ -15,8 +15,6 @@ Table users {
|
|||||||
sex user_sex
|
sex user_sex
|
||||||
phone_number String
|
phone_number String
|
||||||
country countries
|
country countries
|
||||||
auth_provider String
|
|
||||||
provider_token String
|
|
||||||
address user_addresses
|
address user_addresses
|
||||||
preferences user_preferences
|
preferences user_preferences
|
||||||
created_at DateTime [default: `now()`, not null]
|
created_at DateTime [default: `now()`, not null]
|
||||||
@ -76,7 +74,6 @@ Table user_oauth_accounts {
|
|||||||
provider_token String
|
provider_token String
|
||||||
refresh_token String
|
refresh_token String
|
||||||
expires_at DateTime
|
expires_at DateTime
|
||||||
last_login DateTime [default: `now()`, not null]
|
|
||||||
created_at DateTime [default: `now()`, not null]
|
created_at DateTime [default: `now()`, not null]
|
||||||
updated_at DateTime [not null]
|
updated_at DateTime [not null]
|
||||||
user_id String [not null]
|
user_id String [not null]
|
||||||
@ -98,7 +95,7 @@ Table user_sessions {
|
|||||||
|
|
||||||
Table user_preferences {
|
Table user_preferences {
|
||||||
user users [not null]
|
user users [not null]
|
||||||
char_as_partner characters [not null]
|
char_as_partner characters
|
||||||
comment_picture String
|
comment_picture String
|
||||||
enable_watch_history Boolean [not null, default: true]
|
enable_watch_history Boolean [not null, default: true]
|
||||||
enable_search_history Boolean [not null, default: false]
|
enable_search_history Boolean [not null, default: false]
|
||||||
@ -115,7 +112,7 @@ Table user_preferences {
|
|||||||
rating_preferences user_rating_preferences [not null]
|
rating_preferences user_rating_preferences [not null]
|
||||||
country_preferences user_country_preferences [not null]
|
country_preferences user_country_preferences [not null]
|
||||||
user_id String [pk]
|
user_id String [pk]
|
||||||
char_as_partner_id String [not null]
|
char_as_partner_id String
|
||||||
}
|
}
|
||||||
|
|
||||||
Table user_genre_preferences {
|
Table user_genre_preferences {
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `auth_provider` on the `users` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `provider_token` on the `users` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" DROP COLUMN "auth_provider",
|
||||||
|
DROP COLUMN "provider_token";
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `last_login` on the `user_oauth_accounts` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "user_oauth_accounts" DROP COLUMN "last_login";
|
||||||
@ -178,7 +178,6 @@ model UserOauthAccounts {
|
|||||||
provider_token String? @db.VarChar(255)
|
provider_token String? @db.VarChar(255)
|
||||||
refresh_token String? @db.VarChar(255)
|
refresh_token String? @db.VarChar(255)
|
||||||
expires_at DateTime? @db.Timestamptz()
|
expires_at DateTime? @db.Timestamptz()
|
||||||
last_login DateTime @default(now()) @db.Timestamptz()
|
|
||||||
created_at DateTime @default(now()) @db.Timestamptz()
|
created_at DateTime @default(now()) @db.Timestamptz()
|
||||||
updated_at DateTime @updatedAt @db.Timestamptz()
|
updated_at DateTime @updatedAt @db.Timestamptz()
|
||||||
|
|
||||||
@ -204,7 +203,7 @@ model UserSession {
|
|||||||
|
|
||||||
model UserPreference {
|
model UserPreference {
|
||||||
user User @relation(fields: [user_id], references: [id])
|
user User @relation(fields: [user_id], references: [id])
|
||||||
char_as_partner Character @relation(fields: [char_as_partner_id], references: [id])
|
char_as_partner Character? @relation(fields: [char_as_partner_id], references: [id])
|
||||||
comment_picture String? @db.VarChar(255)
|
comment_picture String? @db.VarChar(255)
|
||||||
enable_watch_history Boolean @default(true)
|
enable_watch_history Boolean @default(true)
|
||||||
enable_search_history Boolean @default(false)
|
enable_search_history Boolean @default(false)
|
||||||
@ -222,7 +221,7 @@ model UserPreference {
|
|||||||
country_preferences UserCountryPreference[]
|
country_preferences UserCountryPreference[]
|
||||||
|
|
||||||
user_id String @id @db.Uuid
|
user_id String @id @db.Uuid
|
||||||
char_as_partner_id String @db.Uuid
|
char_as_partner_id String? @db.Uuid
|
||||||
@@map("user_preferences")
|
@@map("user_preferences")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,10 +13,12 @@ export const getUserHeaderInformation = (clientInfo: string): UserHeaderInformat
|
|||||||
const clientInfoHeader = (JSON.parse(clientInfo) as ClientInfoHeader) ?? ("unknown" as string);
|
const clientInfoHeader = (JSON.parse(clientInfo) as ClientInfoHeader) ?? ("unknown" as string);
|
||||||
|
|
||||||
const userHeaderInformation = {
|
const userHeaderInformation = {
|
||||||
ip: clientInfoHeader.ip ?? "unknown",
|
ip: clientInfoHeader.ip,
|
||||||
deviceType: clientInfoHeader.deviceType ?? "unknown",
|
deviceType: clientInfoHeader.deviceType ?? "desktop",
|
||||||
deviceOS: (clientInfoHeader.os ?? "unknown") + " " + (clientInfoHeader.osVersion ?? "unknown"),
|
osType: clientInfoHeader.os ?? "unknown",
|
||||||
browser: (clientInfoHeader.browser ?? "unknown") + " " + (clientInfoHeader.browserVersion ?? "unknown"),
|
osVersion: clientInfoHeader.osVersion ?? "unknown",
|
||||||
|
browserName: clientInfoHeader.browser ?? "unknown",
|
||||||
|
browserVersion: clientInfoHeader.browserVersion ?? "unknown",
|
||||||
};
|
};
|
||||||
|
|
||||||
return userHeaderInformation;
|
return userHeaderInformation;
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
export interface UserHeaderInformation {
|
export interface UserHeaderInformation {
|
||||||
ip: string;
|
ip?: string;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
deviceOS: string;
|
osType: string;
|
||||||
browser: string;
|
osVersion: string;
|
||||||
|
browserName: string;
|
||||||
|
browserVersion: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -12,14 +12,12 @@ export const googleCallbackService = async (
|
|||||||
code: string;
|
code: string;
|
||||||
callbackURI?: string;
|
callbackURI?: string;
|
||||||
},
|
},
|
||||||
userHeaderInfo: UserHeaderInformation
|
userHeaderInfo: UserHeaderInformation,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// get code and state for validation from params and search for state in redis cache
|
// get code and state for validation from params and search for state in redis cache
|
||||||
const state = query.state;
|
const state = query.state;
|
||||||
const codeVerifier = await redis.get(
|
const codeVerifier = await redis.get(`${process.env.APP_NAME}:pkce:${state}`);
|
||||||
`${process.env.APP_NAME}:pkce:${state}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// return error if the state for validation is not found in redis, and delete if found
|
// return error if the state for validation is not found in redis, and delete if found
|
||||||
if (!codeVerifier) throw new AppError(408, "Request timeout");
|
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
|
// create access token with the result of validating the authorization code that compares access code with validator state
|
||||||
const google = googleProvider(query.callbackURI);
|
const google = googleProvider(query.callbackURI);
|
||||||
const tokens = await google.validateAuthorizationCode(
|
const tokens = await google.validateAuthorizationCode(query.code, codeVerifier);
|
||||||
query.code,
|
|
||||||
codeVerifier
|
|
||||||
);
|
|
||||||
|
|
||||||
// get user data from Google using the access token that has been created.
|
// get user data from Google using the access token that has been created.
|
||||||
const accessToken = tokens.accessToken();
|
const accessToken = tokens.accessToken();
|
||||||
const response = await fetch(
|
const response = await fetch("https://openidconnect.googleapis.com/v1/userinfo", {
|
||||||
"https://openidconnect.googleapis.com/v1/userinfo",
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// parse the user data response
|
// parse the user data response
|
||||||
const userData = (await response.json()) as GoogleCallbackUserData;
|
const userData = (await response.json()) as GoogleCallbackUserData;
|
||||||
@ -49,19 +41,17 @@ export const googleCallbackService = async (
|
|||||||
// Provision or authenticate the user in the system
|
// Provision or authenticate the user in the system
|
||||||
return await OAuthUserProvisionService(
|
return await OAuthUserProvisionService(
|
||||||
{
|
{
|
||||||
provider: "google",
|
fullname: userData.name,
|
||||||
providerId: userData.sub,
|
username: `gle_${userData.sub}`,
|
||||||
providerToken: accessToken,
|
|
||||||
providerPayload: userData,
|
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
username: `goo_${userData.sub}`,
|
|
||||||
name: userData.name,
|
|
||||||
avatar: userData.picture,
|
avatar: userData.picture,
|
||||||
password: Math.random()
|
oauthProvider: {
|
||||||
.toString(36)
|
providerName: "google",
|
||||||
.slice(2, 16),
|
sub: userData.sub,
|
||||||
|
token: accessToken,
|
||||||
},
|
},
|
||||||
userHeaderInfo
|
},
|
||||||
|
userHeaderInfo,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorForwarder(error, 500, "Authentication service error");
|
ErrorForwarder(error, 500, "Authentication service error");
|
||||||
|
|||||||
@ -1,39 +1,29 @@
|
|||||||
import { User } from "@prisma/client";
|
|
||||||
import { UserHeaderInformation } from "../../../../helpers/http/userHeader/getUserHeaderInformation/types";
|
import { UserHeaderInformation } from "../../../../helpers/http/userHeader/getUserHeaderInformation/types";
|
||||||
import { findUserService } from "../../../user/services/internal/findUser.service";
|
|
||||||
import { createUserSessionService } from "../../../userSession/services/createUserSession.service";
|
import { createUserSessionService } from "../../../userSession/services/createUserSession.service";
|
||||||
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
||||||
import { createUserViaOauth } from "../../../user/user.types";
|
import { createUserViaOauth } from "../../../user/user.types";
|
||||||
import { createUserService } from "../../../user/services/internal/createUser.service";
|
import { createUserService } from "../../../user/services/internal/createUser.service";
|
||||||
import { AppError } from "../../../../helpers/error/instances/app";
|
import { AppError } from "../../../../helpers/error/instances/app";
|
||||||
|
import { findAuthIdentityByEmailAndProviderRepository } from "../../repositories/READ/findAuthIdentityByEmailAndProvider.repository";
|
||||||
|
|
||||||
export const OAuthUserProvisionService = async (
|
export const OAuthUserProvisionService = async (payload: createUserViaOauth, userHeaderInfo: UserHeaderInformation) => {
|
||||||
payload: createUserViaOauth,
|
|
||||||
userHeaderInfo: UserHeaderInformation
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
const providerId = payload.providerId;
|
const checkExistingUser = await findAuthIdentityByEmailAndProviderRepository(payload.email);
|
||||||
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");
|
|
||||||
|
|
||||||
|
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);
|
const createdUser = await createUserService(payload);
|
||||||
return await createUserSessionService(createdUser.id, userHeaderInfo);
|
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) {
|
} catch (error) {
|
||||||
ErrorForwarder(error);
|
ErrorForwarder(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,18 +3,14 @@ import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
|||||||
import { userModel } from "../../user.model";
|
import { userModel } from "../../user.model";
|
||||||
import { createUserViaRegisterInput } from "../../user.types";
|
import { createUserViaRegisterInput } from "../../user.types";
|
||||||
|
|
||||||
export const createUserViaRegisterRepository = async (
|
export const createUserViaRegisterRepository = async (payload: createUserViaRegisterInput) => {
|
||||||
payload: createUserViaRegisterInput,
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
return await userModel.create({
|
return await userModel.create({
|
||||||
data: {
|
data: {
|
||||||
...payload,
|
...payload,
|
||||||
id: generateUUIDv7(),
|
id: generateUUIDv7(),
|
||||||
preference: {
|
preferences: {
|
||||||
create: {
|
create: {},
|
||||||
id: generateUUIDv7(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,21 +1,17 @@
|
|||||||
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
||||||
import { hashPassword } from "../../../../helpers/security/password/hash";
|
import { hashPassword } from "../../../../helpers/security/password/hash";
|
||||||
import { createUserViaRegisterRepository } from "../../repositories/create/createUserViaRegister.repository";
|
import { createUserViaRegisterRepository } from "../../repositories/create/createUserViaRegister.repository";
|
||||||
import {
|
import { createUserViaOauth, createUserViaRegisterInput } from "../../user.types";
|
||||||
createUserViaOauth,
|
|
||||||
createUserViaRegisterInput,
|
|
||||||
} from "../../user.types";
|
|
||||||
|
|
||||||
export const createUserService = async (
|
export const createUserService = async (payload: createUserViaRegisterInput) => {
|
||||||
payload: createUserViaRegisterInput | createUserViaOauth
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
const hashedPassword = await hashPassword(payload.password);
|
if ("password" in payload && payload.password)
|
||||||
|
|
||||||
return await createUserViaRegisterRepository({
|
return await createUserViaRegisterRepository({
|
||||||
...payload,
|
...payload,
|
||||||
password: hashedPassword,
|
password: await hashPassword(payload.password),
|
||||||
});
|
} as createUserViaRegisterInput);
|
||||||
|
|
||||||
|
return await createUserViaRegisterRepository(payload as Omit<createUserViaOauth, "oauthProvider">);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorForwarder(error);
|
ErrorForwarder(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,20 +10,20 @@ export interface getUserDataOptions {
|
|||||||
export type getUserDataIncludeOptions = "preference" | "roles";
|
export type getUserDataIncludeOptions = "preference" | "roles";
|
||||||
|
|
||||||
export interface createUserViaRegisterInput {
|
export interface createUserViaRegisterInput {
|
||||||
name: string;
|
fullname: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password?: string;
|
||||||
}
|
|
||||||
export interface createUserViaOauth {
|
|
||||||
provider: string;
|
|
||||||
providerId: string;
|
|
||||||
providerToken?: string;
|
|
||||||
providerPayload?: unknown;
|
|
||||||
email: string;
|
|
||||||
username: string;
|
|
||||||
name: string;
|
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
password: string;
|
datebirth?: Date;
|
||||||
|
}
|
||||||
|
export interface createUserViaOauth extends createUserViaRegisterInput {
|
||||||
|
oauthProvider: {
|
||||||
|
providerName: string;
|
||||||
|
sub: string;
|
||||||
|
token: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
expiresAt?: Date;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,24 +5,21 @@ import { createUserSessionRepository } from "../repositories/createUserSession.r
|
|||||||
import { jwtEncode } from "../../../helpers/http/jwt/encode";
|
import { jwtEncode } from "../../../helpers/http/jwt/encode";
|
||||||
import { createUserSessionInRedisService } from "./internal/createUserSessionInRedis.service";
|
import { createUserSessionInRedisService } from "./internal/createUserSessionInRedis.service";
|
||||||
|
|
||||||
export const createUserSessionService = async (
|
export const createUserSessionService = async (userId: string, userHeaderInfo: UserHeaderInformation) => {
|
||||||
userId: string,
|
|
||||||
userHeaderInfo: UserHeaderInformation,
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
// set the date when the token will expire
|
// set the date when the token will expire
|
||||||
const generateTokenExpirationDate =
|
const generateTokenExpirationDate = Date.now() + Number(process.env.SESSION_EXPIRE) * 1000;
|
||||||
Date.now() + Number(process.env.SESSION_EXPIRE) * 1000;
|
|
||||||
|
|
||||||
// construct all data to fit the user session input query
|
// construct all data to fit the user session input query
|
||||||
const constructData = {
|
const constructData = {
|
||||||
userId,
|
user_id: userId,
|
||||||
isAuthenticated: true,
|
device_type: userHeaderInfo.deviceType,
|
||||||
deviceType: userHeaderInfo.deviceType,
|
os_type: userHeaderInfo.osType,
|
||||||
deviceOs: userHeaderInfo.deviceOS,
|
os_version: userHeaderInfo.osVersion,
|
||||||
deviceIp: userHeaderInfo.ip,
|
browser_name: userHeaderInfo.browserName,
|
||||||
browser: userHeaderInfo.browser,
|
browser_version: userHeaderInfo.browserVersion,
|
||||||
validUntil: new Date(generateTokenExpirationDate),
|
ip_login: userHeaderInfo.ip,
|
||||||
|
valid_until: new Date(generateTokenExpirationDate),
|
||||||
} as Prisma.UserSessionUncheckedCreateInput;
|
} as Prisma.UserSessionUncheckedCreateInput;
|
||||||
|
|
||||||
// insert user session into database
|
// insert user session into database
|
||||||
|
|||||||
Reference in New Issue
Block a user