Compare commits
18 commits
main
...
sentry-use
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e1c152e5b | ||
|
|
4bec77bb91 | ||
|
|
bbdaf7dc6c | ||
|
|
01d1c2e03a | ||
|
|
af0542b20d | ||
|
|
b5bbf878a2 | ||
|
|
a66a053ca9 | ||
|
|
d8e6759bbf | ||
|
|
a21087ea25 | ||
|
|
4a21ebae66 | ||
|
|
d8f790b2e4 | ||
|
|
894dfdbd34 | ||
|
|
47e1795503 | ||
|
|
8932ed712f | ||
|
|
9364012118 | ||
|
|
3dd49e2b77 | ||
|
|
a997bec147 | ||
|
|
cf03b9d0d6 |
8 changed files with 235 additions and 2 deletions
|
|
@ -86,6 +86,9 @@ NEXT_PUBLIC_POLOTNO=""
|
||||||
# NOT_SECURED=false
|
# NOT_SECURED=false
|
||||||
API_LIMIT=30 # The limit of the public API hour limit
|
API_LIMIT=30 # The limit of the public API hour limit
|
||||||
|
|
||||||
|
# Sentry Error Tracking Settings (optional)
|
||||||
|
# NEXT_PUBLIC_SENTRY_DSN=""
|
||||||
|
|
||||||
# Payment settings
|
# Payment settings
|
||||||
FEE_AMOUNT=0.05
|
FEE_AMOUNT=0.05
|
||||||
STRIPE_PUBLISHABLE_KEY=""
|
STRIPE_PUBLISHABLE_KEY=""
|
||||||
|
|
@ -120,4 +123,4 @@ POSTIZ_OAUTH_CLIENT_SECRET=""
|
||||||
|
|
||||||
# LINK_DRIP_API_KEY="" # Your LinkDrip API key
|
# LINK_DRIP_API_KEY="" # Your LinkDrip API key
|
||||||
# LINK_DRIP_API_ENDPOINT="https://api.linkdrip.com/v1/" # Your self-hosted LinkDrip API endpoint
|
# LINK_DRIP_API_ENDPOINT="https://api.linkdrip.com/v1/" # Your self-hosted LinkDrip API endpoint
|
||||||
# LINK_DRIP_SHORT_LINK_DOMAIN="dripl.ink" # Your self-hosted LinkDrip domain
|
# LINK_DRIP_SHORT_LINK_DOMAIN="dripl.ink" # Your self-hosted LinkDrip domain
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
|
||||||
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
|
import { TrackService } from '@gitroom/nestjs-libraries/track/track.service';
|
||||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||||
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||||
|
import { clearSentryUserContext } from '@gitroom/nestjs-libraries/sentry/sentry.user.context';
|
||||||
|
|
||||||
@ApiTags('User')
|
@ApiTags('User')
|
||||||
@Controller('/user')
|
@Controller('/user')
|
||||||
|
|
@ -238,6 +239,9 @@ export class UsersController {
|
||||||
|
|
||||||
@Post('/logout')
|
@Post('/logout')
|
||||||
logout(@Res({ passthrough: true }) response: Response) {
|
logout(@Res({ passthrough: true }) response: Response) {
|
||||||
|
// Clear Sentry user context on logout
|
||||||
|
clearSentryUserContext();
|
||||||
|
|
||||||
response.header('logout', 'true');
|
response.header('logout', 'true');
|
||||||
response.cookie('auth', '', {
|
response.cookie('auth', '', {
|
||||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||||
|
|
|
||||||
|
|
@ -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 { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
|
||||||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||||
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
|
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';
|
||||||
|
import { setSentryUserContext } from '@gitroom/nestjs-libraries/sentry/sentry.user.context';
|
||||||
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
|
import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service';
|
||||||
|
|
||||||
export const removeAuth = (res: Response) => {
|
export const removeAuth = (res: Response) => {
|
||||||
|
|
@ -33,6 +34,8 @@ export class AuthMiddleware implements NestMiddleware {
|
||||||
async use(req: Request, res: Response, next: NextFunction) {
|
async use(req: Request, res: Response, next: NextFunction) {
|
||||||
const auth = req.headers.auth || req.cookies.auth;
|
const auth = req.headers.auth || req.cookies.auth;
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
|
// Clear Sentry user context when no auth token is present
|
||||||
|
setSentryUserContext(null);
|
||||||
throw new HttpForbiddenException();
|
throw new HttpForbiddenException();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -70,6 +73,10 @@ export class AuthMiddleware implements NestMiddleware {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
req.org = loadImpersonate.organization;
|
req.org = loadImpersonate.organization;
|
||||||
|
|
||||||
|
// Set Sentry user context for impersonated user
|
||||||
|
setSentryUserContext(user);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +104,12 @@ export class AuthMiddleware implements NestMiddleware {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
req.org = setOrg;
|
req.org = setOrg;
|
||||||
|
|
||||||
|
// Set Sentry user context for this request
|
||||||
|
setSentryUserContext(user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Clear Sentry user context on authentication failure
|
||||||
|
setSentryUserContext(null);
|
||||||
throw new HttpForbiddenException();
|
throw new HttpForbiddenException();
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||||
import { setCookie } from '@gitroom/frontend/components/layout/layout.context';
|
import { setCookie } from '@gitroom/frontend/components/layout/layout.context';
|
||||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||||
|
import { clearSentryUserContext } from '@gitroom/react/sentry/sentry.user.context';
|
||||||
|
export const LogoutComponent = () => {
|
||||||
export const LogoutComponent: FC<{ isIcon?: boolean }> = ({ isIcon }) => {
|
export const LogoutComponent: FC<{ isIcon?: boolean }> = ({ isIcon }) => {
|
||||||
const fetch = useFetch();
|
const fetch = useFetch();
|
||||||
const { isGeneral, isSecured } = useVariables();
|
const { isGeneral, isSecured } = useVariables();
|
||||||
|
|
@ -21,6 +23,9 @@ export const LogoutComponent: FC<{ isIcon?: boolean }> = ({ isIcon }) => {
|
||||||
t('yes_logout', 'Yes logout')
|
t('yes_logout', 'Yes logout')
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
// Clear Sentry user context on logout
|
||||||
|
clearSentryUserContext();
|
||||||
|
|
||||||
if (!isSecured) {
|
if (!isSecured) {
|
||||||
setCookie('auth', '', -10);
|
setCookie('auth', '', -10);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, FC, ReactNode, useContext } from 'react';
|
import { createContext, FC, ReactNode, useContext, useEffect } from 'react';
|
||||||
import { User } from '@prisma/client';
|
import { User } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
pricing,
|
pricing,
|
||||||
PricingInnerInterface,
|
PricingInnerInterface,
|
||||||
} from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
|
} from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
|
||||||
|
import { setSentryUserContext } from '@gitroom/react/sentry/sentry.user.context';
|
||||||
export const UserContext = createContext<
|
export const UserContext = createContext<
|
||||||
| undefined
|
| undefined
|
||||||
| (User & {
|
| (User & {
|
||||||
|
|
@ -37,6 +38,22 @@ export const ContextWrapper: FC<{
|
||||||
tier: pricing[user.tier],
|
tier: pricing[user.tier],
|
||||||
}
|
}
|
||||||
: ({} as any);
|
: ({} 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,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSentryUserContext(null);
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
return <UserContext.Provider value={values}>{children}</UserContext.Provider>;
|
return <UserContext.Provider value={values}>{children}</UserContext.Provider>;
|
||||||
};
|
};
|
||||||
export const useUser = () => useContext(UserContext);
|
export const useUser = () => useContext(UserContext);
|
||||||
|
|
|
||||||
63
libraries/nestjs-libraries/src/sentry/sentry.user.context.ts
Normal file
63
libraries/nestjs-libraries/src/sentry/sentry.user.context.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silently fail if Sentry throws an error - we don't want to break the app
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.setTag('user.activated', null);
|
||||||
|
Sentry.setTag('user.provider', null);
|
||||||
|
Sentry.setTag('user.super_admin', null);
|
||||||
|
} catch {
|
||||||
|
// Silently fail if Sentry throws an error - we don't want to break the app
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* Usage Options:
|
||||||
|
*
|
||||||
|
* 1. Global interceptor (recommended for APIs with consistent auth):
|
||||||
|
* In your app.module.ts:
|
||||||
|
* ```typescript
|
||||||
|
* import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
|
* import { SentryUserInterceptor } from '@gitroom/nestjs-libraries/sentry/sentry.user.interceptor';
|
||||||
|
*
|
||||||
|
* @Module({
|
||||||
|
* providers: [
|
||||||
|
* { provide: APP_INTERCEPTOR, useClass: SentryUserInterceptor },
|
||||||
|
* ],
|
||||||
|
* })
|
||||||
|
* export class AppModule {}
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 2. Controller-level (for specific controllers):
|
||||||
|
* ```typescript
|
||||||
|
* @UseInterceptors(SentryUserInterceptor)
|
||||||
|
* @Controller('users')
|
||||||
|
* export class UsersController {}
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 3. Method-level (for specific routes):
|
||||||
|
* ```typescript
|
||||||
|
* @UseInterceptors(SentryUserInterceptor)
|
||||||
|
* @Get('profile')
|
||||||
|
* getProfile() {}
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@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,75 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as Sentry from '@sentry/nextjs';
|
||||||
|
|
||||||
|
interface UserInfo {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
orgId?: string;
|
||||||
|
role?: string;
|
||||||
|
tier?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silently fail if Sentry throws an error - we don't want to break the app
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.setTag('user.org_id', null);
|
||||||
|
Sentry.setTag('user.role', null);
|
||||||
|
Sentry.setTag('user.tier', null);
|
||||||
|
} catch {
|
||||||
|
// Silently fail if Sentry throws an error - we don't want to break the app
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue