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
+
+
+ ) : (
+
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 c1e63613..3a8751d1 100644
--- a/apps/frontend/src/components/layout/layout.settings.tsx
+++ b/apps/frontend/src/components/layout/layout.settings.tsx
@@ -26,6 +26,7 @@ import { Onboarding } from '@gitroom/frontend/components/onboarding/onboarding';
import { Support } from '@gitroom/frontend/components/layout/support';
import { ContinueProvider } from '@gitroom/frontend/components/layout/continue.provider';
import { isGeneral } from '@gitroom/react/helpers/is.general';
+import { Impersonate } from '@gitroom/frontend/components/layout/impersonate';
dayjs.extend(utc);
dayjs.extend(weekOfYear);
@@ -58,6 +59,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 01456f17..b659ab95 100644
--- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma
+++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma
@@ -37,6 +37,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/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()
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}
)}