* fix: cast prisma user object to user object * chore: improved consistency * chore: cast function renamed --------- Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
448 lines
12 KiB
TypeScript
448 lines
12 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { PrismaService } from 'src/prisma/prisma.service';
|
|
import * as O from 'fp-ts/Option';
|
|
import * as E from 'fp-ts/Either';
|
|
import * as TO from 'fp-ts/TaskOption';
|
|
import * as TE from 'fp-ts/TaskEither';
|
|
import * as T from 'fp-ts/Task';
|
|
import * as A from 'fp-ts/Array';
|
|
import { pipe, constVoid } from 'fp-ts/function';
|
|
import { AuthUser } from 'src/types/AuthUser';
|
|
import { USER_NOT_FOUND } from 'src/errors';
|
|
import { SessionType, User } from './user.model';
|
|
import { USER_UPDATE_FAILED } from 'src/errors';
|
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
|
import { stringToJson, taskEitherValidateArraySeq } from 'src/utils';
|
|
import { UserDataHandler } from './user.data.handler';
|
|
import { User as DbUser } from '@prisma/client';
|
|
|
|
@Injectable()
|
|
export class UserService {
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private readonly pubsub: PubSubService,
|
|
) {}
|
|
|
|
private userDataHandlers: UserDataHandler[] = [];
|
|
|
|
registerUserDataHandler(handler: UserDataHandler) {
|
|
this.userDataHandlers.push(handler);
|
|
}
|
|
|
|
/**
|
|
* Converts a prisma user object to a user object
|
|
*
|
|
* @param dbUser Prisma User object
|
|
* @returns User object
|
|
*/
|
|
convertDbUserToUser(dbUser: DbUser): User {
|
|
const dbCurrentRESTSession = dbUser.currentRESTSession;
|
|
const dbCurrentGQLSession = dbUser.currentGQLSession;
|
|
|
|
return {
|
|
...dbUser,
|
|
currentRESTSession: dbCurrentRESTSession
|
|
? JSON.stringify(dbCurrentRESTSession)
|
|
: null,
|
|
currentGQLSession: dbCurrentGQLSession
|
|
? JSON.stringify(dbCurrentGQLSession)
|
|
: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Find User with given email id
|
|
*
|
|
* @param email User's email
|
|
* @returns Option of found User
|
|
*/
|
|
async findUserByEmail(email: string): Promise<O.None | O.Some<AuthUser>> {
|
|
try {
|
|
const user = await this.prisma.user.findUniqueOrThrow({
|
|
where: {
|
|
email: email,
|
|
},
|
|
});
|
|
return O.some(user);
|
|
} catch (error) {
|
|
return O.none;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find User with given ID
|
|
*
|
|
* @param userUid User ID
|
|
* @returns Option of found User
|
|
*/
|
|
async findUserById(userUid: string): Promise<O.None | O.Some<AuthUser>> {
|
|
try {
|
|
const user = await this.prisma.user.findUniqueOrThrow({
|
|
where: {
|
|
uid: userUid,
|
|
},
|
|
});
|
|
return O.some(user);
|
|
} catch (error) {
|
|
return O.none;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update User with new generated hashed refresh token
|
|
*
|
|
* @param refreshTokenHash Hash of newly generated refresh token
|
|
* @param userUid User uid
|
|
* @returns Either of User with updated refreshToken
|
|
*/
|
|
async UpdateUserRefreshToken(refreshTokenHash: string, userUid: string) {
|
|
try {
|
|
const user = await this.prisma.user.update({
|
|
where: {
|
|
uid: userUid,
|
|
},
|
|
data: {
|
|
refreshToken: refreshTokenHash,
|
|
},
|
|
});
|
|
|
|
return E.right(user);
|
|
} catch (error) {
|
|
return E.left(USER_NOT_FOUND);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new User when logged in via a Magic Link
|
|
*
|
|
* @param email User's Email
|
|
* @returns Created User
|
|
*/
|
|
async createUserViaMagicLink(email: string) {
|
|
const createdUser = await this.prisma.user.create({
|
|
data: {
|
|
email: email,
|
|
providerAccounts: {
|
|
create: {
|
|
provider: 'magic',
|
|
providerAccountId: email,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
return createdUser;
|
|
}
|
|
|
|
/**
|
|
* Create a new User when logged in via a SSO provider
|
|
*
|
|
* @param accessTokenSSO User's access token generated by providers
|
|
* @param refreshTokenSSO User's refresh token generated by providers
|
|
* @param profile Data received from SSO provider on the users account
|
|
* @returns Created User
|
|
*/
|
|
async createUserSSO(
|
|
accessTokenSSO: string,
|
|
refreshTokenSSO: string,
|
|
profile,
|
|
) {
|
|
const userDisplayName = !profile.displayName ? null : profile.displayName;
|
|
const userPhotoURL = !profile.photos ? null : profile.photos[0].value;
|
|
|
|
const createdUser = await this.prisma.user.create({
|
|
data: {
|
|
displayName: userDisplayName,
|
|
email: profile.emails[0].value,
|
|
photoURL: userPhotoURL,
|
|
providerAccounts: {
|
|
create: {
|
|
provider: profile.provider,
|
|
providerAccountId: profile.id,
|
|
providerRefreshToken: refreshTokenSSO,
|
|
providerAccessToken: accessTokenSSO,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
return createdUser;
|
|
}
|
|
|
|
/**
|
|
* Create a new Account for a given User
|
|
*
|
|
* @param user User object
|
|
* @param accessToken User's access token generated by providers
|
|
* @param refreshToken User's refresh token generated by providers
|
|
* @param profile Data received from SSO provider on the users account
|
|
* @returns Created Account
|
|
*/
|
|
async createProviderAccount(
|
|
user: AuthUser,
|
|
accessToken: string,
|
|
refreshToken: string,
|
|
profile,
|
|
) {
|
|
const createdProvider = await this.prisma.account.create({
|
|
data: {
|
|
provider: profile.provider,
|
|
providerAccountId: profile.id,
|
|
providerRefreshToken: refreshToken ? refreshToken : null,
|
|
providerAccessToken: accessToken ? accessToken : null,
|
|
user: {
|
|
connect: {
|
|
uid: user.uid,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
return createdProvider;
|
|
}
|
|
|
|
/**
|
|
* Update User displayName and photoURL
|
|
*
|
|
* @param user User object
|
|
* @param profile Data received from SSO provider on the users account
|
|
* @returns Updated user object
|
|
*/
|
|
async updateUserDetails(user: AuthUser, profile) {
|
|
try {
|
|
const updatedUser = await this.prisma.user.update({
|
|
where: {
|
|
uid: user.uid,
|
|
},
|
|
data: {
|
|
displayName: !profile.displayName ? null : profile.displayName,
|
|
photoURL: !profile.photos ? null : profile.photos[0].value,
|
|
},
|
|
});
|
|
return E.right(updatedUser);
|
|
} catch (error) {
|
|
return E.left(USER_NOT_FOUND);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update a user's sessions
|
|
* @param user User object
|
|
* @param currentRESTSession user's current REST session
|
|
* @param currentGQLSession user's current GQL session
|
|
* @returns a Either of User or error
|
|
*/
|
|
async updateUserSessions(
|
|
user: AuthUser,
|
|
currentSession: string,
|
|
sessionType: string,
|
|
): Promise<E.Right<User> | E.Left<string>> {
|
|
const validatedSession = await this.validateSession(currentSession);
|
|
if (E.isLeft(validatedSession)) return E.left(validatedSession.left);
|
|
|
|
try {
|
|
const sessionObj = {};
|
|
switch (sessionType) {
|
|
case SessionType.GQL:
|
|
sessionObj['currentGQLSession'] = validatedSession.right;
|
|
break;
|
|
case SessionType.REST:
|
|
sessionObj['currentRESTSession'] = validatedSession.right;
|
|
break;
|
|
default:
|
|
return E.left(USER_UPDATE_FAILED);
|
|
}
|
|
|
|
const dbUpdatedUser = await this.prisma.user.update({
|
|
where: { uid: user.uid },
|
|
data: sessionObj,
|
|
});
|
|
|
|
const updatedUser = this.convertDbUserToUser(dbUpdatedUser);
|
|
|
|
// Publish subscription for user updates
|
|
await this.pubsub.publish(`user/${updatedUser.uid}/updated`, updatedUser);
|
|
|
|
return E.right(updatedUser);
|
|
} catch (e) {
|
|
return E.left(USER_UPDATE_FAILED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate and parse currentRESTSession and currentGQLSession
|
|
* @param sessionData string of the session
|
|
* @returns a Either of JSON object or error
|
|
*/
|
|
async validateSession(sessionData: string) {
|
|
const jsonSession = stringToJson(sessionData);
|
|
if (E.isLeft(jsonSession)) return E.left(jsonSession.left);
|
|
|
|
return E.right(jsonSession.right);
|
|
}
|
|
|
|
/**
|
|
* Fetch all the users in the `User` table based on cursor
|
|
* @param cursorID string of userUID or null
|
|
* @param take number of users to query
|
|
* @returns an array of `User` object
|
|
*/
|
|
async fetchAllUsers(cursorID: string, take: number) {
|
|
const fetchedUsers = await this.prisma.user.findMany({
|
|
skip: cursorID ? 1 : 0,
|
|
take: take,
|
|
cursor: cursorID ? { uid: cursorID } : undefined,
|
|
});
|
|
return fetchedUsers;
|
|
}
|
|
|
|
/**
|
|
* Fetch the number of users in db
|
|
* @returns a count (Int) of user records in DB
|
|
*/
|
|
async getUsersCount() {
|
|
const usersCount = await this.prisma.user.count();
|
|
return usersCount;
|
|
}
|
|
|
|
/**
|
|
* Change a user to an admin by toggling isAdmin param to true
|
|
* @param userUID user UID
|
|
* @returns a Either of `User` object or error
|
|
*/
|
|
async makeAdmin(userUID: string) {
|
|
try {
|
|
const elevatedUser = await this.prisma.user.update({
|
|
where: {
|
|
uid: userUID,
|
|
},
|
|
data: {
|
|
isAdmin: true,
|
|
},
|
|
});
|
|
return E.right(elevatedUser);
|
|
} catch (error) {
|
|
return E.left(USER_NOT_FOUND);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch all the admin users
|
|
* @returns an array of admin users
|
|
*/
|
|
async fetchAdminUsers() {
|
|
const admins = this.prisma.user.findMany({
|
|
where: {
|
|
isAdmin: true,
|
|
},
|
|
});
|
|
|
|
return admins;
|
|
}
|
|
|
|
/**
|
|
* Deletes a user account by UID
|
|
* @param uid User UID
|
|
* @returns an Either of string or boolean
|
|
*/
|
|
async deleteUserAccount(uid: string) {
|
|
try {
|
|
await this.prisma.user.delete({
|
|
where: {
|
|
uid: uid,
|
|
},
|
|
});
|
|
return E.right(true);
|
|
} catch (e) {
|
|
return E.left(USER_NOT_FOUND);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user deletion error messages when the data handlers are initialised in respective modules
|
|
* @param user User Object
|
|
* @returns an TaskOption of string array
|
|
*/
|
|
getUserDeletionErrors(user: AuthUser): TO.TaskOption<string[]> {
|
|
return pipe(
|
|
this.userDataHandlers,
|
|
A.map((handler) =>
|
|
pipe(
|
|
handler.canAllowUserDeletion(user),
|
|
TO.matchE(
|
|
() => TE.right(undefined),
|
|
(error) => TE.left(error),
|
|
),
|
|
),
|
|
),
|
|
taskEitherValidateArraySeq,
|
|
TE.matchE(
|
|
(e) => TO.some(e),
|
|
() => TO.none,
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Deletes a user by UID
|
|
* @param user User Object
|
|
* @returns an TaskEither of string or boolean
|
|
*/
|
|
deleteUserByUID(user: AuthUser) {
|
|
return pipe(
|
|
this.getUserDeletionErrors(user),
|
|
TO.matchEW(
|
|
() =>
|
|
pipe(
|
|
this.userDataHandlers,
|
|
A.map((handler) => handler.onUserDelete(user)),
|
|
T.sequenceArray,
|
|
T.map(constVoid),
|
|
TE.fromTask,
|
|
) as TE.TaskEither<never, void>,
|
|
(errors): TE.TaskEither<string[], void> => TE.left(errors),
|
|
),
|
|
|
|
TE.chainW(() => () => this.deleteUserAccount(user.uid)),
|
|
|
|
TE.chainFirst(() =>
|
|
TE.fromTask(() =>
|
|
this.pubsub.publish(`user/${user.uid}/deleted`, <User>{
|
|
uid: user.uid,
|
|
displayName: user.displayName,
|
|
email: user.email,
|
|
photoURL: user.photoURL,
|
|
isAdmin: user.isAdmin,
|
|
createdOn: user.createdOn,
|
|
currentGQLSession: user.currentGQLSession,
|
|
currentRESTSession: user.currentRESTSession,
|
|
}),
|
|
),
|
|
),
|
|
|
|
TE.mapLeft((errors) => errors.toString()),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Change the user from an admin by toggling isAdmin param to false
|
|
* @param userUID user UID
|
|
* @returns a Either of `User` object or error
|
|
*/
|
|
async removeUserAsAdmin(userUID: string) {
|
|
try {
|
|
const user = await this.prisma.user.update({
|
|
where: {
|
|
uid: userUID,
|
|
},
|
|
data: {
|
|
isAdmin: false,
|
|
},
|
|
});
|
|
return E.right(user);
|
|
} catch (error) {
|
|
return E.left(USER_NOT_FOUND);
|
|
}
|
|
}
|
|
}
|