feat: impersonate

This commit is contained in:
Nevo David 2024-05-24 19:20:24 +07:00
parent fe10030a68
commit 85cbc3bc55
11 changed files with 345 additions and 39 deletions

View File

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

View File

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

View File

@ -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 (
<div className="px-[23px]">
<div className="bg-forth h-[52px] flex justify-center items-center border-input border rounded-[8px]">
<div className="relative flex flex-col w-[600px]">
<div className="relative z-[999]">
{user?.impersonate ? (
<div className="text-center flex justify-center items-center gap-[20px]">
<div>Currently Impersonating</div>
<div>
<div
className="px-[10px] rounded-[4px] bg-red-500 text-white cursor-pointer"
onClick={stopImpersonating}
>
X
</div>
</div>
</div>
) : (
<Input
autoComplete="off"
placeholder="Write the user details"
name="impersonate"
disableForm={true}
label=""
removeError={true}
value={name}
onChange={(e) => setName(e.target.value)}
/>
)}
</div>
{!!data?.length && (
<>
<div
className="bg-black/80 fixed left-0 top-0 w-full h-full z-[998]"
onClick={() => setName('')}
/>
<div className="absolute top-[100%] w-full left-0 bg-sixth border border-[#172034] text-white z-[999]">
{mapData?.map((user: any) => (
<div
onClick={setUser(user.id)}
key={user.id}
className="p-[10px] border-b border-[#172034] hover:bg-tableBorder cursor-pointer"
>
user: {user.id.split('-').at(-1)} - {user.name} -{' '}
{user.email}
</div>
))}
</div>
</>
)}
</div>
</div>
</div>
);
};

View File

@ -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 }) => {
<Onboarding />
<Support />
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-[12px] text-white flex flex-col">
{user?.admin && <Impersonate />}
<div className="px-[23px] flex h-[80px] items-center justify-between z-[200] sticky top-0 bg-primary">
<Link href="/" className="text-2xl flex items-center gap-[10px]">
<div>

View File

@ -15,6 +15,7 @@ export const UserContext = createContext<
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
totalChannels: number;
isLifetime?: boolean;
impersonate: boolean;
})
>(undefined);

View File

@ -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: {

View File

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

View File

@ -36,6 +36,7 @@ model User {
providerName Provider
name String?
lastName String?
isSuperAdmin Boolean @default(false)
bio String?
audience Int @default(0)
pictureId String?

View File

@ -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: {

View File

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

View File

@ -44,7 +44,7 @@ export const Input: FC<
return (
<div className="flex flex-col gap-[6px]">
<div className={`${interClass} text-[14px]`}>{label}</div>
{!!label && (<div className={`${interClass} text-[14px]`}>{label}</div>)}
<div
className={clsx(
'bg-input h-[44px] border-fifth border rounded-[4px] text-inputText placeholder-inputText flex items-center justify-center',