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 ( +