feat: impersonate
This commit is contained in:
parent
fe10030a68
commit
85cbc3bc55
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const UserContext = createContext<
|
|||
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
||||
totalChannels: number;
|
||||
isLifetime?: boolean;
|
||||
impersonate: boolean;
|
||||
})
|
||||
>(undefined);
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ model User {
|
|||
providerName Provider
|
||||
name String?
|
||||
lastName String?
|
||||
isSuperAdmin Boolean @default(false)
|
||||
bio String?
|
||||
audience Int @default(0)
|
||||
pictureId String?
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in New Issue