Skip to content

Instantly share code, notes, and snippets.

@carlos-talavera
Last active December 13, 2025 15:41
Show Gist options
  • Select an option

  • Save carlos-talavera/c97b7730045225b8d63e32b6e64f5c35 to your computer and use it in GitHub Desktop.

Select an option

Save carlos-talavera/c97b7730045225b8d63e32b6e64f5c35 to your computer and use it in GitHub Desktop.
Example of implementing Permit.io in NestJS
// This is to retrieve all the available permissions so you can fetch them in the frontend
import { AuthGuard } from '@/shared/auth/auth.guard';
import { Session } from '@/shared/decorators/session.decorator';
import { CheckPermissionsDto } from '@/shared/permit/dto/check-permissions.dto';
import { PermitService } from '@/shared/permit/permit.service';
import { SessionWithMetadata } from '@/shared/types';
import { Body, Controller, Post, Res, UseGuards } from '@nestjs/common';
import type { Response } from 'express';
@Controller('permit')
export class PermitController {
constructor(private readonly permitService: PermitService) {}
@Post('check-permissions')
@UseGuards(AuthGuard)
public async checkPermissions(
@Body() checkPermissionsDto: CheckPermissionsDto,
@Session() session: SessionWithMetadata,
@Res() res: Response,
): Promise<void> {
const permit = this.permitService.getPermitClient();
const permittedList = await Promise.all(
checkPermissionsDto.resourcesAndActions.map((resourceAndAction) =>
permit.check(
session.metadata.uuid,
resourceAndAction.action,
resourceAndAction.resource,
),
),
);
res.status(200).json({ permittedList });
}
}
import { PermitService } from '@/shared/permit/permit.service';
import { getSessionFromContext } from '@/shared/utils/session.util';
import {
BadRequestException,
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
InternalServerErrorException,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class PermitGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly permitService: PermitService,
) {}
public async canActivate(context: ExecutionContext): Promise<boolean> {
const user = (await getSessionFromContext(context)).metadata; // Example using SuperTokens
if (!user) {
throw new UnauthorizedException('No user context found');
}
const permission = this.reflector.get<{ action: string; resource: string }>(
'permission',
context.getHandler(),
);
if (!permission) {
throw new BadRequestException('No permission found');
}
try {
const permit = this.permitService.getPermitClient();
const permitted = await permit.check(
user.uuid,
permission.action,
permission.resource,
);
if (!permitted) {
throw new ForbiddenException('Insufficient permissions');
}
return true;
} catch (error) {
if (error instanceof ForbiddenException) {
throw error;
}
console.error(error);
throw new InternalServerErrorException(
'An error occurred while checking permissions',
);
}
}
}
import Environment from '@/shared/config/models/environment.enum';
import { ConfigService } from '@nestjs/config';
import { AREAS } from '@/shared/constants/area.constant';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { isEqual, keys, omit } from 'lodash';
import { Permit, ResourceCreate, RoleCreate } from 'permitio';
import { kebabCase } from "lodash";
function removeAccentMarks(str: string) {
const normalizedStr = str.normalize("NFD");
const accentRemovedStr = normalizedStr.replace(/[\u0300-\u036f]/g, "");
return accentRemovedStr;
}
function formatAreaNameForPermit(name: string) {
return kebabCase(removeAccentMarks(name));
}
const NODE_ENV_ENVIRONMENT_ID_MAP: Record<Environment, string> = {
[Environment.LOCAL]: 'dev',
[Environment.DEVELOPMENT]: 'dev',
[Environment.STAGING]: 'staging',
[Environment.PRODUCTION]: 'production',
};
const RESOURCES: ResourceCreate[] = [
{
key: 'user',
name: 'user',
actions: {
read: {},
create: {},
update: {},
delete: {},
},
},
{
key: 'client',
name: 'client',
actions: {
read: {},
create: {},
update: {},
transfer: {},
},
},
{
key: 'mdmx-invoice',
name: 'mdmx-invoice',
actions: {
read: {},
create: {},
},
}
];
const MANAGEMENT_PERMISSIONS: string[] = [
'user:read',
'user:create',
'user:update',
'user:delete',
'client:read',
'client:create',
'client:update',
'client:transfer',
];
const SALES_PERMISSIONS: string[] = [
'client:read',
'client:create',
'client:update',
];
const ROLES: RoleCreate[] = [
{
key: formatAreaNameForPermit(AREAS_NAME_TRANSLATED_NAME_MAP.MANAGEMENT),
name: AREAS_NAME_TRANSLATED_NAME_MAP.MANAGEMENT,
permissions: MANAGEMENT_PERMISSIONS,
},
{
key: formatAreaNameForPermit(AREAS_NAME_TRANSLATED_NAME_MAP.SALES),
name: AREAS_NAME_TRANSLATED_NAME_MAP.SALES,
permissions: SALES_PERMISSIONS,
}
];
@Injectable()
export class PermitService implements OnModuleInit {
private permitClient: Permit;
private projectId: string;
private environmentId: string;
constructor(private readonly configService: ConfigService) {
this.permitClient = new Permit({
pdp: this.configService.get('PERMIT_IO_PDP_URL'),
token: this.configService.get('PERMIT_IO_API_KEY'),
});
this.projectId = 'default';
this.environmentId = NODE_ENV_ENVIRONMENT_ID_MAP[process.env.NODE_ENV];
}
public async onModuleInit() {
if (
this.configService.get('SEED_PERMIT_RESOURCES_FEATURE_ENABLED') === 'true'
) {
await this.seedResources();
}
if (
this.configService.get('SEED_PERMIT_ROLES_FEATURE_ENABLED') === 'true'
) {
await this.seedRoles();
}
}
public getPermitClient() {
return this.permitClient;
}
private async seedResources() {
const response = await fetch(
`https://api.permit.io/v2/schema/${this.projectId}/${this.environmentId}/resources`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${this.configService.get('PERMIT_IO_API_KEY')}`,
},
},
);
const existingResources = (await response.json()) as ResourceCreate[];
for (const resource of RESOURCES) {
const existingResource = existingResources.find(
(r) => r.key === resource.key,
);
if (!existingResource) {
await this.permitClient.api.createResource(resource);
} else {
const existingActionKeys = keys(existingResource.actions).sort();
const resourceActionKeys = keys(resource.actions).sort();
if (!isEqual(existingActionKeys, resourceActionKeys)) {
await this.permitClient.api.updateResource(
resource.key,
omit(resource, ['key']),
);
}
}
}
for (const existingResource of existingResources) {
if (!RESOURCES.some((r) => r.key === existingResource.key)) {
await this.permitClient.api.deleteResource(existingResource.key);
}
}
}
private async seedRoles() {
const response = await fetch(
`https://api.permit.io/v2/schema/${this.projectId}/${this.environmentId}/roles`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${this.configService.get('PERMIT_IO_API_KEY')}`,
},
},
);
const existingRoles = (await response.json()) as RoleCreate[];
for (const role of ROLES) {
const existingRole = existingRoles.find((r) => r.key === role.key);
if (!existingRole) {
await this.permitClient.api.createRole(role);
} else {
const existingPermissionKeys = keys(existingRole.permissions).sort();
const rolePermissionKeys = keys(role.permissions).sort();
if (!isEqual(existingPermissionKeys, rolePermissionKeys)) {
await this.permitClient.api.updateRole(role.key, omit(role, ['key']));
}
}
}
for (const existingRole of existingRoles) {
if (!ROLES.some((r) => r.key === existingRole.key)) {
await this.permitClient.api.deleteRole(existingRole.key);
}
}
}
}
import { UserCreateInput } from '@/api/users/dto/user-create.input';
import { UserSelect } from '@/api/users/models/user-select.model';
import { User } from '@/api/users/models/user.model';
import { UserService } from '@/api/users/user.service';
import { AuthGuard } from '@/shared/auth/auth.guard';
import { GraphQLFields } from '@/shared/decorators/graphql-fields.decorator';
import { Session } from '@/shared/decorators/session.decorator';
import { PermitGuard } from '@/shared/permit/permit.guard';
import { IGraphQLFields, SessionWithMetadata } from '@/shared/types';
import { SetMetadata, UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
@Resolver(() => User)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@UseGuards(AuthGuard, PermitGuard)
@SetMetadata('permission', { action: 'create', resource: 'user' })
@Mutation(() => User)
public async createUser(
@Args('data') data: UserCreateInput,
@GraphQLFields() { fields }: IGraphQLFields<UserSelect>,
@Session() session: SessionWithMetadata,
): Promise<User> {
return this.userService.create(data, fields, session.metadata);
}
}
// Inside the create method
const permit = this.permitService.getPermitClient();
await permit.api.syncUser({
key: user.uuid,
email: user.email,
first_name: user.name,
role_assignments: [
{
role: formatAreaNameForPermit(user.area.name),
tenant: 'default', // Use tenant ID if dealing with multi-tenancy
},
],
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment