Skip to content

Commit

Permalink
Merge branch 'feat/impersonate'
Browse files Browse the repository at this point in the history
  • Loading branch information
Nevo David committed May 24, 2024
2 parents 43b13b8 + 85cbc3b commit 3d0cc13
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 37 deletions.
40 changes: 38 additions & 2 deletions apps/backend/src/api/routes/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Get,
HttpException,
Post,
Query,
Req,
Res,
} from '@nestjs/common';
Expand All @@ -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';
Expand All @@ -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);
Expand All @@ -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,
};
}

Expand All @@ -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,
Expand Down
88 changes: 57 additions & 31 deletions apps/backend/src/services/auth/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -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 {
const user = AuthService.verifyJWT(auth) as User | null;
const orgHeader = req.cookies.showorg || req.headers.showorg;
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) {
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];
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;
// 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');
// @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 ;
}
console.log('Request...');
next();
}

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.org = setOrg;
} catch (err) {
throw new Error('Unauthorized');
}
console.log('Request...');
next();
}
}
117 changes: 117 additions & 0 deletions apps/frontend/src/components/layout/impersonate.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
2 changes: 2 additions & 0 deletions apps/frontend/src/components/layout/layout.settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/components/layout/user.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const UserContext = createContext<
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
totalChannels: number;
isLifetime?: boolean;
impersonate: boolean;
})
>(undefined);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading

0 comments on commit 3d0cc13

Please sign in to comment.