From 85cbc3bc553ccbf92555da0a6b248f584605567c Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 24 May 2024 19:20:24 +0700 Subject: [PATCH 1/2] feat: impersonate --- .../src/api/routes/users.controller.ts | 40 +++++- .../src/services/auth/auth.middleware.ts | 92 +++++++++----- .../src/components/layout/impersonate.tsx | 117 ++++++++++++++++++ .../src/components/layout/layout.settings.tsx | 2 + .../src/components/layout/user.context.tsx | 1 + .../organizations/organization.repository.ts | 71 +++++++++++ .../organizations/organization.service.ts | 4 + .../src/database/prisma/schema.prisma | 1 + .../database/prisma/users/users.repository.ts | 39 +++++- .../database/prisma/users/users.service.ts | 15 ++- .../react-shared-libraries/src/form/input.tsx | 2 +- 11 files changed, 345 insertions(+), 39 deletions(-) create mode 100644 apps/frontend/src/components/layout/impersonate.tsx diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index 20b6fcd2..a048b748 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -4,6 +4,7 @@ import { Get, HttpException, Post, + Query, Req, Res, } from '@nestjs/common'; @@ -12,7 +13,7 @@ import { Organization, User } from '@prisma/client'; import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service'; -import { Response } from 'express'; +import { Response, Request } from 'express'; import { AuthService } from '@gitroom/backend/services/auth/auth.service'; import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability'; @@ -39,7 +40,8 @@ export class UsersController { @Get('/self') async getSelf( @GetUserFromRequest() user: User, - @GetOrgFromRequest() organization: Organization + @GetOrgFromRequest() organization: Organization, + @Req() req: Request ) { if (!organization) { throw new HttpException('Organization not found', 401); @@ -56,6 +58,8 @@ export class UsersController { role: organization?.users[0]?.role, // @ts-ignore isLifetime: !!organization?.subscription?.isLifetime, + admin: !!user.isSuperAdmin, + impersonate: !!req.cookies.impersonate, }; } @@ -64,6 +68,38 @@ export class UsersController { return this._userService.getPersonal(user.id); } + @Get('/impersonate') + async getImpersonate( + @GetUserFromRequest() user: User, + @Query('name') name: string + ) { + if (!user.isSuperAdmin) { + throw new HttpException('Unauthorized', 401); + } + + return this._userService.getImpersonateUser(name); + } + + @Post('/impersonate') + async setImpersonate( + @GetUserFromRequest() user: User, + @Body('id') id: string, + @Res({ passthrough: true }) response: Response + ) { + if (!user.isSuperAdmin) { + throw new HttpException('Unauthorized', 401); + } + + response.cookie('impersonate', id, { + domain: + '.' + new URL(removeSubdomain(process.env.FRONTEND_URL!)).hostname, + secure: true, + httpOnly: true, + sameSite: 'none', + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), + }); + } + @Post('/personal') async changePersonal( @GetUserFromRequest() user: User, diff --git a/apps/backend/src/services/auth/auth.middleware.ts b/apps/backend/src/services/auth/auth.middleware.ts index d97f9396..d2040774 100644 --- a/apps/backend/src/services/auth/auth.middleware.ts +++ b/apps/backend/src/services/auth/auth.middleware.ts @@ -1,45 +1,71 @@ import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; -import {AuthService} from "@gitroom/helpers/auth/auth.service"; -import {User} from '@prisma/client'; -import {OrganizationService} from "@gitroom/nestjs-libraries/database/prisma/organizations/organization.service"; +import { AuthService } from '@gitroom/helpers/auth/auth.service'; +import { User } from '@prisma/client'; +import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; +import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service'; @Injectable() export class AuthMiddleware implements NestMiddleware { - constructor( - private _organizationService: OrganizationService, - ) { + constructor( + private _organizationService: OrganizationService, + private _userService: UsersService + ) {} + async use(req: Request, res: Response, next: NextFunction) { + const auth = req.headers.auth || req.cookies.auth; + if (!auth) { + throw new Error('Unauthorized'); } - async use(req: Request, res: Response, next: NextFunction) { - const auth = req.headers.auth || req.cookies.auth; - if (!auth) { - throw new Error('Unauthorized'); + try { + let user = AuthService.verifyJWT(auth) as User | null; + const orgHeader = req.cookies.showorg || req.headers.showorg; + + if (!user) { + throw new Error('Unauthorized'); + } + + if (user?.isSuperAdmin && req.cookies.impersonate) { + const loadImpersonate = await this._organizationService.getUserOrg( + req.cookies.impersonate + ); + + if (loadImpersonate) { + user = loadImpersonate.user; + user.isSuperAdmin = true; + delete user.password; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + req.user = user; + + // @ts-ignore + loadImpersonate.organization.users = loadImpersonate.organization.users.filter(f => f.userId === user.id); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + req.org = loadImpersonate.organization; + next(); + return ; } - try { - const user = AuthService.verifyJWT(auth) as User | null; - const orgHeader = req.cookies.showorg || req.headers.showorg; + } - if (!user) { - throw new Error('Unauthorized'); - } + delete user.password; + const organization = ( + await this._organizationService.getOrgsByUserId(user.id) + ).filter((f) => !f.users[0].disabled); + const setOrg = + organization.find((org) => org.id === orgHeader) || organization[0]; - delete user.password; - const organization = (await this._organizationService.getOrgsByUserId(user.id)).filter(f => !f.users[0].disabled); - const setOrg = organization.find((org) => org.id === orgHeader) || organization[0]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + req.user = user; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - req.user = user; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - req.org = setOrg; - } - catch (err) { - throw new Error('Unauthorized'); - } - console.log('Request...'); - next(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + req.org = setOrg; + } catch (err) { + throw new Error('Unauthorized'); } + console.log('Request...'); + next(); + } } diff --git a/apps/frontend/src/components/layout/impersonate.tsx b/apps/frontend/src/components/layout/impersonate.tsx new file mode 100644 index 00000000..b8252b78 --- /dev/null +++ b/apps/frontend/src/components/layout/impersonate.tsx @@ -0,0 +1,117 @@ +import { Input } from '@gitroom/react/form/input'; +import { useCallback, useMemo, useState } from 'react'; +import useSWR from 'swr'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { useUser } from '@gitroom/frontend/components/layout/user.context'; + +export const Impersonate = () => { + const fetch = useFetch(); + const [name, setName] = useState(''); + const user = useUser(); + + const load = useCallback(async () => { + if (!name) { + return []; + } + + const value = await (await fetch(`/user/impersonate?name=${name}`)).json(); + return value; + }, [name]); + + const stopImpersonating = useCallback(async () => { + await fetch(`/user/impersonate`, { + method: 'POST', + body: JSON.stringify({ id: '' }), + }); + + window.location.reload(); + }, []); + + const setUser = useCallback( + (userId: string) => async () => { + await fetch(`/user/impersonate`, { + method: 'POST', + body: JSON.stringify({ id: userId }), + }); + + window.location.reload(); + }, + [] + ); + + const { data } = useSWR(`/impersonate-${name}`, load, { + refreshWhenHidden: false, + revalidateOnMount: true, + revalidateOnReconnect: false, + revalidateOnFocus: false, + refreshWhenOffline: false, + revalidateIfStale: false, + refreshInterval: 0, + }); + + const mapData = useMemo(() => { + return data?.map( + (curr: any) => ({ + id: curr.id, + name: curr.user.name, + email: curr.user.email, + }), + [] + ); + }, [data]); + + return ( +
+
+
+
+ {user?.impersonate ? ( +
+
Currently Impersonating
+
+
+ X +
+
+
+ ) : ( + setName(e.target.value)} + /> + )} +
+ {!!data?.length && ( + <> +
setName('')} + /> +
+ {mapData?.map((user: any) => ( +
+ user: {user.id.split('-').at(-1)} - {user.name} -{' '} + {user.email} +
+ ))} +
+ + )} +
+
+
+ ); +}; diff --git a/apps/frontend/src/components/layout/layout.settings.tsx b/apps/frontend/src/components/layout/layout.settings.tsx index 0c8b0a5c..4ef4cac3 100644 --- a/apps/frontend/src/components/layout/layout.settings.tsx +++ b/apps/frontend/src/components/layout/layout.settings.tsx @@ -24,6 +24,7 @@ import { ShowLinkedinCompany } from '@gitroom/frontend/components/launches/helpe import { SettingsComponent } from '@gitroom/frontend/components/layout/settings.component'; import { Onboarding } from '@gitroom/frontend/components/onboarding/onboarding'; import { Support } from '@gitroom/frontend/components/layout/support'; +import { Impersonate } from '@gitroom/frontend/components/layout/impersonate'; dayjs.extend(utc); dayjs.extend(weekOfYear); @@ -55,6 +56,7 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
+ {user?.admin && }
diff --git a/apps/frontend/src/components/layout/user.context.tsx b/apps/frontend/src/components/layout/user.context.tsx index 146eb192..6dd4ff8b 100644 --- a/apps/frontend/src/components/layout/user.context.tsx +++ b/apps/frontend/src/components/layout/user.context.tsx @@ -15,6 +15,7 @@ export const UserContext = createContext< role: 'USER' | 'ADMIN' | 'SUPERADMIN'; totalChannels: number; isLifetime?: boolean; + impersonate: boolean; }) >(undefined); diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index 261b51f6..a3d208a2 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -12,6 +12,77 @@ export class OrganizationRepository { private _user: PrismaRepository<'user'> ) {} + getUserOrg(id: string) { + return this._userOrg.model.userOrganization.findFirst({ + where: { + id, + }, + select: { + user: true, + organization: { + include: { + users: { + select: { + id: true, + disabled: true, + role: true, + userId: true, + }, + }, + subscription: { + select: { + subscriptionTier: true, + totalChannels: true, + isLifetime: true, + }, + }, + }, + }, + }, + }); + } + + getImpersonateUser(name: string) { + return this._userOrg.model.userOrganization.findMany({ + where: { + user: { + OR: [ + { + name: { + contains: name, + }, + }, + { + email: { + contains: name, + }, + }, + { + id: { + contains: name, + }, + }, + ], + }, + }, + select: { + id: true, + organization: { + select: { + id: true, + }, + }, + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + }); + } + async getOrgsByUserId(userId: string) { return this._organization.model.organization.findMany({ where: { diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts index 315f3071..18c947fe 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts @@ -33,6 +33,10 @@ export class OrganizationService { return this._organizationRepository.getOrgById(id); } + getUserOrg(id: string) { + return this._organizationRepository.getUserOrg(id); + } + getOrgsByUserId(userId: string) { return this._organizationRepository.getOrgsByUserId(userId); } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 1a71c00a..18ca29d2 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -36,6 +36,7 @@ model User { providerName Provider name String? lastName String? + isSuperAdmin Boolean @default(false) bio String? audience Int @default(0) pictureId String? diff --git a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts index eb4238ee..0c2d297c 100644 --- a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts @@ -5,12 +5,49 @@ import { AuthService } from '@gitroom/helpers/auth/auth.service'; import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto'; import { allTagsOptions } from '@gitroom/nestjs-libraries/database/prisma/marketplace/tags.list'; import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto'; -import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto'; @Injectable() export class UsersRepository { constructor(private _user: PrismaRepository<'user'>) {} + getImpersonateUser(name: string) { + return this._user.model.user.findMany({ + where: { + OR: [ + { + name: { + contains: name, + }, + }, + { + email: { + contains: name, + }, + }, + { + id: { + contains: name, + }, + }, + ], + }, + select: { + id: true, + name: true, + email: true, + }, + take: 10, + }); + } + + getUserById(id: string) { + return this._user.model.user.findFirst({ + where: { + id, + }, + }); + } + getUserByEmail(email: string) { return this._user.model.user.findFirst({ where: { diff --git a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts index cac770fc..097582c2 100644 --- a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts @@ -3,16 +3,27 @@ import { UsersRepository } from '@gitroom/nestjs-libraries/database/prisma/users import { Provider } from '@prisma/client'; import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto'; import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto'; -import { NewConversationDto } from '@gitroom/nestjs-libraries/dtos/marketplace/new.conversation.dto'; +import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository'; @Injectable() export class UsersService { - constructor(private _usersRepository: UsersRepository) {} + constructor( + private _usersRepository: UsersRepository, + private _organizationRepository: OrganizationRepository + ) {} getUserByEmail(email: string) { return this._usersRepository.getUserByEmail(email); } + getUserById(id: string) { + return this._usersRepository.getUserById(id); + } + + getImpersonateUser(name: string) { + return this._organizationRepository.getImpersonateUser(name); + } + getUserByProvider(providerId: string, provider: Provider) { return this._usersRepository.getUserByProvider(providerId, provider); } diff --git a/libraries/react-shared-libraries/src/form/input.tsx b/libraries/react-shared-libraries/src/form/input.tsx index 66e7febd..4e788211 100644 --- a/libraries/react-shared-libraries/src/form/input.tsx +++ b/libraries/react-shared-libraries/src/form/input.tsx @@ -44,7 +44,7 @@ export const Input: FC< return (
-
{label}
+ {!!label && (
{label}
)}
Date: Fri, 24 May 2024 19:22:07 +0700 Subject: [PATCH 2/2] feat: openai fix --- libraries/nestjs-libraries/src/openai/openai.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/openai/openai.service.ts b/libraries/nestjs-libraries/src/openai/openai.service.ts index 718e8185..eb559060 100644 --- a/libraries/nestjs-libraries/src/openai/openai.service.ts +++ b/libraries/nestjs-libraries/src/openai/openai.service.ts @@ -3,7 +3,7 @@ import OpenAI from 'openai'; import { shuffle } from 'lodash'; const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, + apiKey: process.env.OPENAI_API_KEY || 'sk-proj-', }); @Injectable()