Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/backend/src/api/routes/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
import { clearSentryUserContext } from '@gitroom/nestjs-libraries/sentry/sentry.user.context';

@ApiTags('User')
@Controller('/user')
Expand Down Expand Up @@ -199,6 +200,9 @@ export class UsersController {

@Post('/logout')
logout(@Res({ passthrough: true }) response: Response) {
// Clear Sentry user context on logout
clearSentryUserContext();

response.cookie('auth', '', {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
...(!process.env.NOT_SECURED
Expand Down
12 changes: 12 additions & 0 deletions apps/backend/src/services/auth/auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
import { setSentryUserContext } from '@gitroom/nestjs-libraries/sentry/sentry.user.context';

export const removeAuth = (res: Response) => {
res.cookie('auth', '', {
Expand Down Expand Up @@ -33,6 +34,8 @@ export class AuthMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
const auth = req.headers.auth || req.cookies.auth;
if (!auth) {
// Clear Sentry user context when no auth token is present
setSentryUserContext(null);
throw new HttpForbiddenException();
}
try {
Expand Down Expand Up @@ -70,6 +73,10 @@ export class AuthMiddleware implements NestMiddleware {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
req.org = loadImpersonate.organization;

// Set Sentry user context for impersonated user
setSentryUserContext(user);

next();
return;
}
Expand Down Expand Up @@ -97,7 +104,12 @@ export class AuthMiddleware implements NestMiddleware {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
req.org = setOrg;

// Set Sentry user context for this request
setSentryUserContext(user);
} catch (err) {
// Clear Sentry user context on authentication failure
setSentryUserContext(null);
throw new HttpForbiddenException();
}
next();
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/components/layout/logout.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useVariables } from '@gitroom/react/helpers/variable.context';
import { setCookie } from '@gitroom/frontend/components/layout/layout.context';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { clearSentryUserContext } from '@gitroom/react/sentry/sentry.user.context';
export const LogoutComponent = () => {
const fetch = useFetch();
const { isGeneral, isSecured } = useVariables();
Expand All @@ -21,6 +22,9 @@ export const LogoutComponent = () => {
t('yes_logout', 'Yes logout')
)
) {
// Clear Sentry user context on logout
Comment thread
egelhaus marked this conversation as resolved.
clearSentryUserContext();

if (!isSecured) {
setCookie('auth', '', -10);
} else {
Expand Down
22 changes: 21 additions & 1 deletion apps/frontend/src/components/layout/user.context.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use client';

import { createContext, FC, ReactNode, useContext } from 'react';
import { createContext, FC, ReactNode, useContext, useEffect } from 'react';
import { User } from '@prisma/client';
import {
pricing,
PricingInnerInterface,
} from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
import { setSentryUserContext } from '@gitroom/react/sentry/sentry.user.context';
export const UserContext = createContext<
| undefined
| (User & {
Expand All @@ -18,6 +19,7 @@ export const UserContext = createContext<
impersonate: boolean;
allowTrial: boolean;
isTrailing: boolean;
admin?: boolean; // Add admin field from backend response
Comment thread
egelhaus marked this conversation as resolved.
Outdated
})
>(undefined);
export const ContextWrapper: FC<{
Expand All @@ -27,6 +29,7 @@ export const ContextWrapper: FC<{
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
publicApi: string;
totalChannels: number;
admin: boolean; // Add admin field from backend response
Comment thread
egelhaus marked this conversation as resolved.
Outdated
};
children: ReactNode;
}> = ({ user, children }) => {
Expand All @@ -36,6 +39,23 @@ export const ContextWrapper: FC<{
tier: pricing[user.tier],
}
: ({} as any);

// Set Sentry user context whenever user changes
useEffect(() => {
if (user) {
setSentryUserContext({
id: user.id,
email: user.email,
orgId: user.orgId,
role: user.role,
tier: user.tier,
admin: user.admin, // Use admin field from backend response
});
} else {
setSentryUserContext(null);
}
}, [user]);

return <UserContext.Provider value={values}>{children}</UserContext.Provider>;
};
export const useUser = () => useContext(UserContext);
55 changes: 55 additions & 0 deletions libraries/nestjs-libraries/src/sentry/sentry.user.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as Sentry from '@sentry/nestjs';
import { User } from '@prisma/client';

/**
* Sets user context for Sentry for the current request.
* This will include user information in all error reports and events.
* Only executes if Sentry DSN is configured.
*
* @param user - The user object from the database
*/
export const setSentryUserContext = (user: User | null) => {
// Only set context if Sentry is configured
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) {
Comment thread
egelhaus marked this conversation as resolved.
return;
}

if (!user) {
// Clear user context when no user is present
Sentry.setUser(null);
return;
}

Sentry.setUser({
id: user.id,
email: user.email,
username: user.email, // Use email as username since that's the primary identifier
// Add additional useful context
ip_address: undefined, // Let Sentry auto-detect IP
});

// Also set additional tags for better filtering in Sentry
Sentry.setTag('user.activated', user.activated);
Sentry.setTag('user.provider', user.providerName || 'local');

if (user.isSuperAdmin) {
Sentry.setTag('user.super_admin', true);
}
};

/**
* Clears the Sentry user context.
* Useful when logging out or switching users.
* Only executes if Sentry DSN is configured.
*/
export const clearSentryUserContext = () => {
// Only clear context if Sentry is configured
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) {
Comment thread
egelhaus marked this conversation as resolved.
return;
}

Sentry.setUser(null);
Sentry.setTag('user.activated', null);
Sentry.setTag('user.provider', null);
Sentry.setTag('user.super_admin', null);
Comment thread
egelhaus marked this conversation as resolved.
Outdated
Comment thread
egelhaus marked this conversation as resolved.
Outdated
Comment thread
egelhaus marked this conversation as resolved.
Outdated
};
24 changes: 24 additions & 0 deletions libraries/nestjs-libraries/src/sentry/sentry.user.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Request } from 'express';
import { User } from '@prisma/client';
import { setSentryUserContext } from './sentry.user.context';

/**
* Interceptor that automatically sets Sentry user context for all requests.
* This interceptor runs after authentication middleware has set req.user.
*/
@Injectable()
export class SentryUserInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest<Request>();

// Get user from request (set by auth middleware)
const user = (request as any).user as User | undefined;
Comment thread
egelhaus marked this conversation as resolved.

// Set Sentry user context for this request
setSentryUserContext(user || null);

return next.handle();
}
}
73 changes: 73 additions & 0 deletions libraries/react-shared-libraries/src/sentry/sentry.user.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use client';

import * as Sentry from '@sentry/nextjs';

interface UserInfo {
id: string;
email: string;
orgId?: string;
role?: string;
tier?: string;
admin?: boolean;
}

/**
* Sets user context for Sentry in the frontend.
* This will include user information in all error reports and events.
* Only executes if Sentry DSN is configured.
*
* @param user - The user object from the API
*/
export const setSentryUserContext = (user: UserInfo | null) => {
// Only set context if Sentry is configured
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) {
return;
}

if (!user) {
// Clear user context when no user is present
Sentry.setUser(null);
return;
}

Sentry.setUser({
id: user.id,
email: user.email,
username: user.email, // Use email as username since that's the primary identifier
});

// Also set additional tags for better filtering in Sentry
if (user.orgId) {
Sentry.setTag('user.org_id', user.orgId);
}

if (user.role) {
Sentry.setTag('user.role', user.role);
}

if (user.tier) {
Sentry.setTag('user.tier', user.tier);
}

if (user.admin) {
Sentry.setTag('user.admin', true);
}
};

/**
* Clears the Sentry user context.
* Useful when logging out or switching users.
* Only executes if Sentry DSN is configured.
*/
export const clearSentryUserContext = () => {
// Only clear context if Sentry is configured
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) {
return;
}

Sentry.setUser(null);
Sentry.setTag('user.org_id', '');
Sentry.setTag('user.role', '');
Sentry.setTag('user.tier', '');
Sentry.setTag('user.admin', '');
};