feat/sentry user implementation
This commit is contained in:
parent
f7899681a5
commit
cf03b9d0d6
7 changed files with 199 additions and 1 deletions
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', '', {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -21,6 +22,9 @@ export const LogoutComponent = () => {
|
|||
t('yes_logout', 'Yes logout')
|
||||
)
|
||||
) {
|
||||
// Clear Sentry user context on logout
|
||||
clearSentryUserContext();
|
||||
|
||||
if (!isSecured) {
|
||||
setCookie('auth', '', -10);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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 & {
|
||||
|
|
@ -18,6 +19,7 @@ export const UserContext = createContext<
|
|||
impersonate: boolean;
|
||||
allowTrial: boolean;
|
||||
isTrailing: boolean;
|
||||
admin: boolean; // Add admin field from backend response
|
||||
})
|
||||
>(undefined);
|
||||
export const ContextWrapper: FC<{
|
||||
|
|
@ -27,6 +29,7 @@ export const ContextWrapper: FC<{
|
|||
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
||||
publicApi: string;
|
||||
totalChannels: number;
|
||||
admin: boolean; // Add admin field from backend response
|
||||
};
|
||||
children: ReactNode;
|
||||
}> = ({ user, children }) => {
|
||||
|
|
@ -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
libraries/nestjs-libraries/src/sentry/sentry.user.context.ts
Normal file
55
libraries/nestjs-libraries/src/sentry/sentry.user.context.ts
Normal file
|
|
@ -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) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
Sentry.setUser(null);
|
||||
Sentry.setTag('user.activated', undefined);
|
||||
Sentry.setTag('user.provider', undefined);
|
||||
Sentry.setTag('user.super_admin', undefined);
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
||||
// Set Sentry user context for this request
|
||||
setSentryUserContext(user || null);
|
||||
|
||||
return next.handle();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
'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 (check at runtime for frontend)
|
||||
if (typeof window !== 'undefined' && !window.location.origin.includes('localhost')) {
|
||||
// For production, check if Sentry DSN exists in environment or is initialized
|
||||
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
return;
|
||||
}
|
||||
} else if (typeof process !== 'undefined' && !process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
// For server-side or development
|
||||
return;
|
||||
}
|
||||
|
||||
Sentry.setUser(null);
|
||||
Sentry.setTag('user.org_id', undefined);
|
||||
Sentry.setTag('user.role', undefined);
|
||||
Sentry.setTag('user.tier', undefined);
|
||||
Sentry.setTag('user.admin', undefined);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue