Package Exports
- @ahksolution/permissions-sdk
- @ahksolution/permissions-sdk/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@ahksolution/permissions-sdk) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@ahksolution/permissions-sdk
gRPC client SDK for the AHK Solution Permissions Microservice. Provides NestJS integration for:
- JWT Authentication - Validate tokens via gRPC (no JWT secret needed in consuming services)
- Permission Checks - RBAC and ABAC support via gRPC
Installation
# From private npm registry
npm install @ahksolution/permissions-sdk
# Or using pnpm
pnpm add @ahksolution/permissions-sdkPeer Dependencies (must be installed in your project):
pnpm add @nestjs/microservices @grpc/grpc-js @grpc/proto-loaderQuick Start
1. Register the Module
// app.module.ts
import { Module } from '@nestjs/common';
import { PermissionsClientModule } from '@ahksolution/permissions-sdk';
@Module({
imports: [
// Static configuration
PermissionsClientModule.register({
url: 'localhost:50051',
}),
],
})
export class AppModule {}With async configuration (recommended for production):
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PermissionsClientModule } from '@ahksolution/permissions-sdk';
@Module({
imports: [
ConfigModule.forRoot(),
PermissionsClientModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
url: config.get<string>('PERMISSIONS_SERVICE_URL', 'localhost:50051'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}2. Apply Guards Globally (Recommended)
For full authentication and authorization, apply both guards globally:
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import {
PermissionsClientModule,
JwtAuthGuard,
PermissionsGuard,
} from '@ahksolution/permissions-sdk';
@Module({
imports: [PermissionsClientModule.register({ url: 'localhost:50051' })],
providers: [
// Order matters: authentication first, then authorization
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: PermissionsGuard },
],
})
export class AppModule {}3. Use Decorators in Controllers
import { Controller, Get, Post, Body } from '@nestjs/common';
import { Public, CurrentUser, RequirePermissions, JwtUserData } from '@ahksolution/permissions-sdk';
@Controller('orders')
export class OrdersController {
// Public route - bypasses JWT authentication
@Public()
@Get('health')
health() {
return { status: 'ok' };
}
// Authenticated route - no specific permission required
@Get('me')
getProfile(@CurrentUser() user: JwtUserData) {
return {
id: user.id,
email: user.email,
roles: user.roles,
};
}
// Authenticated + requires specific permission
@Post()
@RequirePermissions('orders:create')
createOrder(@CurrentUser() user: JwtUserData, @Body() dto: CreateOrderDto) {
return this.orderService.create(user.id, dto);
}
// Get specific user property
@Get('my-orders')
@RequirePermissions('orders:read')
getMyOrders(@CurrentUser('id') userId: string) {
return this.orderService.findByUser(userId);
}
}JWT Authentication
The SDK provides JWT authentication that validates tokens via gRPC call to the permissions service. No JWT secret is required in consuming services - all validation happens centrally.
How It Works
JwtAuthGuardextracts the token fromAuthorization: Bearer <token>header- Calls
ValidateTokengRPC method on the permissions service - Permissions service verifies the token and returns user data
- User data (with roles and permissions) is attached to
request.user
JwtUserData Type
interface JwtUserData {
id: string;
email: string | null;
phone: string | null;
userType: string;
status: string;
isProfileComplete: boolean;
roles: RoleInfo[]; // User's roles
permissions: string[]; // User's permission codes
hasAllAccess: boolean; // True if user has wildcard access
}
interface RoleInfo {
id: string;
code: string;
name: string;
isSystem: boolean;
}Decorators
| Decorator | Description |
|---|---|
@Public() |
Mark route as public (bypass JWT authentication) |
@CurrentUser() |
Get full user object from request |
@CurrentUser('id') |
Get specific property from user |
Permission Checking
Using Decorators (Recommended)
import {
RequirePermissions,
RequireAnyPermission,
RequireAllPermissions,
} from '@ahksolution/permissions-sdk';
@Controller('orders')
export class OrdersController {
// Require a single permission
@Post()
@RequirePermissions('orders:create')
create() {}
// Require ALL permissions (AND logic) - default behavior
@Post(':id/approve')
@RequirePermissions(['orders:read', 'orders:approve'])
approve() {}
// Require ANY of the permissions (OR logic)
@Delete(':id')
@RequirePermissions(['orders:delete', 'admin:full'], { mode: 'any' })
delete() {}
// Using alias decorators for clarity
@Post(':id/export')
@RequireAllPermissions(['orders:read', 'orders:export'])
export() {}
@Post(':id/cancel')
@RequireAnyPermission(['orders:cancel', 'orders:manage'])
cancel() {}
}Using the Client Service
Inject PermissionsGrpcClient to check permissions programmatically:
import { Injectable } from '@nestjs/common';
import { PermissionsGrpcClient } from '@ahksolution/permissions-sdk';
@Injectable()
export class OrderService {
constructor(private readonly permissions: PermissionsGrpcClient) {}
async createOrder(userId: string, orderData: CreateOrderDto) {
// Simple boolean check
const canCreate = await this.permissions.hasPermission(userId, 'orders:create');
if (!canCreate) {
throw new ForbiddenException('You do not have permission to create orders');
}
// Continue with order creation...
}
async deleteOrder(userId: string, orderId: string) {
// Check multiple permissions (user needs ANY of these)
const canDelete = await this.permissions.hasAnyPermission(userId, [
'orders:delete',
'orders:manage',
'admin:full',
]);
if (!canDelete) {
throw new ForbiddenException('You do not have permission to delete orders');
}
// Continue with deletion...
}
}API Reference
Guards
| Guard | Description |
|---|---|
JwtAuthGuard |
Validates JWT tokens via gRPC. Attaches user to request. |
PermissionsGuard |
Checks permissions based on @RequirePermissions decorator. |
PermissionsGrpcClient
| Method | Description |
|---|---|
validateToken(token) |
Validates JWT and returns ValidateTokenResult |
hasPermission(userId, permissionCode) |
Returns boolean - does user have this permission? |
hasAllPermissions(userId, permissionCodes) |
Returns boolean - does user have ALL permissions? |
hasAnyPermission(userId, permissionCodes) |
Returns boolean - does user have ANY permission? |
checkPermission(userId, permissionCode, options?) |
Returns full EvaluationResult with details |
checkBulkPermissions(userId, permissionCodes, options?) |
Returns results for multiple permissions |
getEffectivePermissions(userId) |
Returns all permissions and roles for a user |
getUserInfo(userId) |
Returns complete user profile with roles & permissions |
getUserRoles(userId) |
Returns user's roles only |
getUserPermissions(userId) |
Returns user's permission codes only |
getBulkUserInfo(userIds, options?) |
Returns user data for multiple users with optional flags |
createUser(options) |
Creates a new user with validations and optional magic link |
Decorators
| Decorator | Description |
|---|---|
@Public() |
Mark route as public (bypass JWT auth) |
@CurrentUser() |
Get authenticated user from request |
@CurrentUser('property') |
Get specific property from user |
@RequirePermissions(permissions, options?) |
Require permission(s) with configurable mode |
@RequireAllPermissions(permissions) |
Shorthand for mode: 'all' |
@RequireAnyPermission(permissions) |
Shorthand for mode: 'any' |
Options
interface RequirePermissionsOptions {
// 'all' = user must have ALL permissions (default)
// 'any' = user must have at least ONE permission
mode?: 'all' | 'any';
// Custom error message when denied
errorMessage?: string;
// Include request params as resource context for ABAC
includeResourceContext?: boolean;
}Advanced Usage
Full Evaluation Result
Get detailed information about permission decisions:
const result = await this.permissions.checkPermission(userId, 'orders:create');
console.log(result);
// {
// allowed: true,
// source: 'rbac', // 'rbac' | 'abac' | 'break-glass' | 'denied'
// matchedRoles: ['ADMIN'],
// matchedPolicies: [],
// reason: 'Permission granted via role(s): ADMIN',
// evaluationTimeMs: 3
// }ABAC Context
Pass resource and request context for attribute-based access control:
const result = await this.permissions.checkPermission(userId, 'documents:read', {
resource: {
id: 'doc-123',
type: 'document',
ownerId: 'user-456',
department: 'engineering',
},
request: {
ip: '192.168.1.100',
method: 'GET',
path: '/api/documents/doc-123',
},
});Get All User Permissions
const effective = await this.permissions.getEffectivePermissions(userId);
console.log(effective);
// {
// permissions: ['users:read', 'users:create', 'orders:read'],
// roles: [
// { id: '...', code: 'USER', name: 'User', isSystem: false },
// { id: '...', code: 'ORDER_VIEWER', name: 'Order Viewer', isSystem: false }
// ],
// version: 1,
// computedAt: Date
// }User Data Methods (v1.2.0+)
Fetch user data directly by userId without token validation. Useful for service-to-service calls.
import { Injectable, NotFoundException } from '@nestjs/common';
import { PermissionsGrpcClient } from '@ahksolution/permissions-sdk';
@Injectable()
export class UserProfileService {
constructor(private readonly permissions: PermissionsGrpcClient) {}
// Get complete user profile with roles and permissions
async getUserProfile(userId: string) {
const result = await this.permissions.getUserInfo(userId);
if (!result.found) {
throw new NotFoundException(result.errorMessage); // 'USER_NOT_FOUND' | 'USER_INACTIVE'
}
return result.user;
// {
// id: '...',
// email: 'user@example.com',
// phone: '+1234567890',
// userType: 'CUSTOMER',
// status: 'ACTIVE',
// isProfileComplete: true,
// roles: [{ id, code, name, isSystem }],
// permissions: ['orders:read', 'orders:create'],
// hasAllAccess: false
// }
}
// Get only user's roles
async getUserRoleNames(userId: string) {
const result = await this.permissions.getUserRoles(userId);
if (!result.found) {
throw new NotFoundException(result.errorMessage);
}
return result.roles.map((r) => r.name); // ['Admin', 'Manager']
}
// Get only user's permissions
async canAccessFeature(userId: string, featurePermission: string) {
const result = await this.permissions.getUserPermissions(userId);
if (!result.found) return false;
// Check if user has all access or the specific permission
return result.hasAllAccess || result.permissions.includes(featurePermission);
}
}Bulk User Data (v1.4.0+)
Fetch data for multiple users in a single gRPC call with optional flags to control what data is returned.
import { Injectable, NotFoundException } from '@nestjs/common';
import { PermissionsGrpcClient } from '@ahksolution/permissions-sdk';
@Injectable()
export class TeamService {
constructor(private readonly permissions: PermissionsGrpcClient) {}
// Get basic info for multiple users (no roles/permissions)
async getTeamMembers(userIds: string[]) {
const result = await this.permissions.getBulkUserInfo(userIds);
return Object.entries(result.users)
.filter(([_, item]) => item.found)
.map(([userId, item]) => ({
id: item.user!.id,
email: item.user!.email,
name: item.user!.profile?.fullName,
}));
}
// Get users with their roles included
async getTeamWithRoles(userIds: string[]) {
const result = await this.permissions.getBulkUserInfo(userIds, {
includeRoles: true,
});
return Object.entries(result.users)
.filter(([_, item]) => item.found)
.map(([userId, item]) => ({
id: item.user!.id,
email: item.user!.email,
roles: item.user!.roles.map((r) => r.name),
}));
}
// Get users with both roles and permissions
async getTeamWithFullAccess(userIds: string[]) {
const result = await this.permissions.getBulkUserInfo(userIds, {
includeRoles: true,
includePermissions: true,
});
return Object.entries(result.users).map(([userId, item]) => {
if (!item.found) {
return { userId, error: item.errorMessage };
}
return {
userId,
email: item.user!.email,
roles: item.user!.roles,
permissions: item.user!.permissions,
hasAllAccess: item.user!.hasAllAccess,
};
});
}
// Handle mixed results (some users found, some not)
async validateUserIds(userIds: string[]) {
const result = await this.permissions.getBulkUserInfo(userIds);
const found: string[] = [];
const notFound: string[] = [];
const inactive: string[] = [];
for (const [userId, item] of Object.entries(result.users)) {
if (item.found) {
found.push(userId);
} else if (item.errorMessage === 'USER_INACTIVE') {
inactive.push(userId);
} else {
notFound.push(userId);
}
}
return { found, notFound, inactive };
}
}Options:
| Option | Type | Default | Description |
|---|---|---|---|
includeRoles |
boolean |
false |
Include user roles in response |
includePermissions |
boolean |
false |
Include user permissions in response |
Return Types:
interface GetBulkUserInfoResult {
users: Record<string, BulkUserInfoItem>;
}
interface BulkUserInfoItem {
found: boolean;
errorMessage?: string; // 'USER_NOT_FOUND' | 'USER_INACTIVE' | 'INTERNAL_ERROR'
user?: BulkUserData; // Only populated if found=true
}
interface BulkUserData {
id: string;
email: string | null;
phone: string | null;
userType: string;
status: string;
isProfileComplete: boolean;
roles: RoleInfo[]; // Empty array if includeRoles=false
permissions: string[]; // Empty array if includePermissions=false
hasAllAccess: boolean;
profile?: UserProfileData;
}
interface GetUserInfoResult {
found: boolean;
errorMessage?: string; // 'USER_NOT_FOUND' | 'USER_INACTIVE' | 'INTERNAL_ERROR'
user?: JwtUserData; // Only populated if found=true
}
interface GetUserRolesResult {
found: boolean;
errorMessage?: string;
roles: RoleInfo[];
}
interface GetUserPermissionsResult {
found: boolean;
errorMessage?: string;
permissions: string[];
hasAllAccess: boolean;
}Create User (v1.5.0+)
Create new users via gRPC with full validation support including email uniqueness, magic link sending, role assignment, and organization assignment.
import { Injectable, BadRequestException, ConflictException } from '@nestjs/common';
import { PermissionsGrpcClient, UserType } from '@ahksolution/permissions-sdk';
@Injectable()
export class UserManagementService {
constructor(private readonly permissions: PermissionsGrpcClient) {}
// Create an admin user with magic link
async createAdminUser(email: string, firstName: string, lastName: string) {
const result = await this.permissions.createUser({
firstName,
lastName,
email,
userType: UserType.ADMIN,
sendMagicLink: true, // Default: true
});
if (!result.success) {
// Handle specific error codes
if (result.errorCode === 'EMAIL_EXISTS') {
throw new ConflictException('A user with this email already exists');
}
throw new BadRequestException(result.errorMessage);
}
return {
userId: result.user!.id,
email: result.user!.email,
magicLinkSent: result.magicLinkSent,
};
}
// Create a customer user with roles and organization
async createCustomerWithRoles(data: {
firstName: string;
lastName: string;
email?: string;
phone?: string;
departmentId?: string;
designationId?: string;
roleIds?: string[];
}) {
const result = await this.permissions.createUser({
firstName: data.firstName,
lastName: data.lastName,
email: data.email,
phone: data.phone,
userType: UserType.CUSTOMER,
departmentId: data.departmentId,
designationId: data.designationId,
roleIds: data.roleIds,
sendMagicLink: false, // Don't send magic link for customers
});
if (!result.success) {
this.handleCreateUserError(result.errorCode, result.errorMessage);
}
return result.user;
}
private handleCreateUserError(errorCode?: string, errorMessage?: string): never {
switch (errorCode) {
case 'EMAIL_EXISTS':
throw new ConflictException('Email already registered');
case 'PHONE_EXISTS':
throw new ConflictException('Phone number already registered');
case 'INVALID_ROLE':
throw new BadRequestException('One or more role IDs are invalid');
case 'INVALID_ORG':
throw new BadRequestException('Invalid department or designation ID');
case 'VALIDATION_ERROR':
throw new BadRequestException(errorMessage ?? 'Validation failed');
default:
throw new BadRequestException(errorMessage ?? 'Failed to create user');
}
}
}Options:
interface CreateUserOptions {
firstName: string; // Required
lastName: string; // Required
email?: string; // Required if phone not provided
phone?: string; // Required if email not provided
userType: UserType; // Required: UserType.ADMIN or UserType.CUSTOMER
departmentId?: string; // Optional: Department for org assignment
designationId?: string; // Optional: Designation for org assignment
roleIds?: string[]; // Optional: Role IDs to assign to user
sendMagicLink?: boolean; // Optional: Send magic link email (default: true)
}
enum UserType {
ADMIN = 'ADMIN',
CUSTOMER = 'CUSTOMER',
}Return Types:
interface CreateUserResult {
success: boolean;
errorCode?: string; // 'EMAIL_EXISTS' | 'PHONE_EXISTS' | 'INVALID_ROLE' | 'INVALID_ORG' | 'VALIDATION_ERROR' | 'INTERNAL_ERROR'
errorMessage?: string;
user?: CreatedUserData; // Only populated if success=true
magicLinkSent: boolean;
}
interface CreatedUserData {
id: string;
email: string | null;
phone: string | null;
userType: string;
status: string;
profile?: UserProfileData;
}Error Codes:
| Error Code | Description |
|---|---|
EMAIL_EXISTS |
A user with this email already exists |
PHONE_EXISTS |
A user with this phone number exists |
INVALID_ROLE |
One or more role IDs are invalid |
INVALID_ORG |
Invalid department or designation ID |
VALIDATION_ERROR |
General validation error (see message) |
INTERNAL_ERROR |
Server-side error during user creation |
Environment Variables
| Variable | Description | Default |
|---|---|---|
PERMISSIONS_SERVICE_URL |
gRPC server address | localhost:50051 |
Error Handling
The guard throws ForbiddenException when permission is denied:
// Default error
throw new ForbiddenException('Access denied. Required permission(s): orders:create');
// Custom error message
@RequirePermissions('orders:create', {
errorMessage: 'You need order creation privileges'
})Complete Example
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
PermissionsClientModule,
JwtAuthGuard,
PermissionsGuard,
} from '@ahksolution/permissions-sdk';
@Module({
imports: [
ConfigModule.forRoot(),
PermissionsClientModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
url: config.get('PERMISSIONS_SERVICE_URL', 'localhost:50051'),
}),
inject: [ConfigService],
}),
],
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: PermissionsGuard },
],
})
export class AppModule {}
// users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import {
Public,
CurrentUser,
RequirePermissions,
RequireAnyPermission,
JwtUserData,
} from '@ahksolution/permissions-sdk';
@Controller('users')
export class UsersController {
// Public endpoint - no auth required
@Public()
@Get('health')
health() {
return { status: 'ok' };
}
// Auth required, no specific permission
@Get('profile')
getProfile(@CurrentUser() user: JwtUserData) {
return user;
}
// Auth + specific permission required
@Get()
@RequirePermissions('users:list')
findAll() {
return this.userService.findAll();
}
@Post()
@RequirePermissions('users:create')
create(@Body() dto: CreateUserDto, @CurrentUser('id') createdBy: string) {
return this.userService.create(dto, createdBy);
}
@Get(':id')
@RequireAnyPermission(['users:read', 'users:manage'])
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
}Important Notes
No JWT Secret Required: The SDK validates tokens via gRPC call to the permissions service. You don't need to configure JWT secrets in consuming services.
Guard Order Matters: When using both guards globally,
JwtAuthGuardmust run beforePermissionsGuard(authentication before authorization).Permissions Service Required: This SDK is a client for the AHK Solution Permissions Microservice. It will not function without a running instance exposing a gRPC endpoint.
Same-Pod Deployment: For optimal performance, deploy consuming services in the same pod/network as the permissions service to minimize gRPC latency.
License
MIT