najm-auth
v0.1.12
Published
Authentication and authorization library for najm framework
Maintainers
Readme
najm-auth
Authentication and authorization library for the najm framework.
Features
- 🔐 JWT Authentication - Access and refresh token management
- 👤 Role-Based Access Control (RBAC) - Simple role-based guards
- 🔑 Permission-Based Authorization - Fine-grained permission system with wildcard support
- 🍪 Cookie Management - Secure refresh token cookie handling
- 🔒 Password Encryption - bcrypt password hashing
- 🎯 Decorator-Based - Clean, declarative API using TypeScript decorators
- 🗄️ Database Agnostic - Implement your own repository interfaces
Installation
npm install najm-auth
# or
bun add najm-authQuick Start
1. Environment Variables
Create a .env file:
JWT_ACCESS_SECRET=your-secret-access-key
ACCESS_EXPIRES_IN=15m
JWT_REFRESH_SECRET=your-secret-refresh-key
REFRESH_EXPIRES_IN=1y2. Database Setup
Create the required database tables:
-- Users table
CREATE TABLE users (
id UUID PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role_id UUID REFERENCES roles(id),
status VARCHAR(50),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Roles table
CREATE TABLE roles (
id UUID PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Permissions table
CREATE TABLE permissions (
id UUID PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
action VARCHAR(50) NOT NULL,
resource VARCHAR(100) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Role-Permission junction table
CREATE TABLE role_permissions (
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
-- Tokens table
CREATE TABLE tokens (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
token TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL
);3. Implement Repository Interfaces
You need to implement the repository interfaces for your database/ORM:
import { Repository } from 'najm-api';
import { ITokenRepository, TokenData, AuthUser } from 'najm-auth';
import { db } from './db'; // your database instance
@Repository()
export class TokenRepository implements ITokenRepository {
declare db: any;
async storeRefreshToken(tokenData: TokenData): Promise<any> {
return await this.db
.insert(tokens)
.values(tokenData)
.onConflictDoUpdate({
target: tokens.userId,
set: { token: tokenData.token, expiresAt: tokenData.expiresAt }
});
}
async getRefreshToken(userId: string | number): Promise<string | null> {
const [token] = await this.db
.select()
.from(tokens)
.where(eq(tokens.userId, userId));
return token?.token || null;
}
async revokeToken(userId: string | number): Promise<any> {
return await this.db.delete(tokens).where(eq(tokens.userId, userId));
}
async isUserExists(userId: string | number): Promise<boolean> {
const [user] = await this.db
.select({ id: users.id })
.from(users)
.where(eq(users.id, userId));
return !!user;
}
async getRoleNameById(userId: string | number): Promise<string | null> {
const [role] = await this.db
.select({ roleName: roles.name })
.from(users)
.leftJoin(roles, eq(users.roleId, roles.id))
.where(eq(users.id, userId));
return role?.roleName || null;
}
async getUserPermissions(userId: string | number): Promise<string[]> {
const [user] = await this.db
.select({ roleId: users.roleId })
.from(users)
.where(eq(users.id, userId));
if (!user?.roleId) return [];
const perms = await this.db
.select({ name: permissions.name })
.from(rolePermissions)
.leftJoin(permissions, eq(rolePermissions.permissionId, permissions.id))
.where(eq(rolePermissions.roleId, user.roleId));
return perms.map(p => p.name).filter(Boolean);
}
async getUser(userId: string | number): Promise<AuthUser | null> {
const [user] = await this.db
.select({
id: users.id,
email: users.email,
status: users.status,
roleId: users.roleId,
roleName: roles.name,
})
.from(users)
.leftJoin(roles, eq(users.roleId, roles.id))
.where(eq(users.id, userId));
return user ? { ...user, role: user.roleName } : null;
}
}4. Create Controllers
import { Controller, Get, Post, Body, User, Params } from 'najm-api';
import { isAuth, Role, Permission, AuthService, EncryptionService } from 'najm-auth';
@Controller('/api/auth')
export class AuthController {
constructor(
private authService: AuthService,
private encryptionService: EncryptionService,
private userService: UserService // Your user service
) {}
@Post('/login')
async login(@Body() body: { email: string; password: string }) {
const user = await this.userService.getByEmail(body.email);
// Verify password
const isValid = await this.encryptionService.comparePassword(
body.password,
user.password
);
if (!isValid) {
throw new Error('Invalid credentials');
}
// Generate tokens
const tokens = await this.authService.loginUser(user.id);
return {
data: tokens,
message: 'Login successful',
};
}
@Get('/refresh')
async refresh() {
const tokens = await this.authService.refreshTokens();
return { data: tokens };
}
@Get('/logout/:id')
async logout(@Params('id') id: string) {
await this.authService.logoutUser(id);
return { message: 'Logged out successfully' };
}
@Get('/me')
@isAuth()
async getProfile(@User() user: any) {
const profile = await this.authService.getUserProfile(user);
return { data: profile };
}
}Usage Examples
Role-Based Guards
import { Controller, Get } from 'najm-api';
import { isAuth, Role, isAdmin, isTeacher, isStaff } from 'najm-auth';
@Controller('/api/users')
export class UserController {
// Require authentication
@Get('/profile')
@isAuth()
async getProfile() {
return { message: 'Authenticated user only' };
}
// Require specific role
@Get('/admin-only')
@Role('admin')
async adminRoute() {
return { message: 'Admin only' };
}
// Multiple roles allowed
@Get('/staff-only')
@Role('admin', 'principal', 'teacher')
async staffRoute() {
return { message: 'Staff members only' };
}
// Shorthand decorators
@Get('/teachers')
@isTeacher()
async teachersOnly() {
return { message: 'Teachers only' };
}
@Get('/admins')
@isAdmin()
async adminsOnly() {
return { message: 'Admins only' };
}
@Get('/all-staff')
@isStaff()
async allStaff() {
return { message: 'All staff members' };
}
}Permission-Based Guards
Permissions follow the action:resource format (e.g., read:users, write:classes).
import { Controller, Get, Post, Put, Delete } from 'najm-api';
import { Permission } from 'najm-auth';
@Controller('/api/users')
export class UserController {
@Get('/')
@Permission('read:users')
async listUsers() {
return { data: [] };
}
@Post('/')
@Permission('write:users')
async createUser() {
return { message: 'User created' };
}
@Put('/:id')
@Permission('update:users')
async updateUser() {
return { message: 'User updated' };
}
@Delete('/:id')
@Permission('delete:users')
async deleteUser() {
return { message: 'User deleted' };
}
}Wildcard Permissions
// Permission patterns:
// "read:*" - Can read any resource
// "*:users" - Can perform any action on users
// "*:*" - Super admin (all permissions)
@Controller('/api/admin')
export class AdminController {
@Get('/dashboard')
@Permission('*:*')
async dashboard() {
return { message: 'Super admin dashboard' };
}
}Password Hashing
import { EncryptionService } from 'najm-auth';
@Service()
export class UserService {
constructor(private encryptionService: EncryptionService) {}
async createUser(data: { email: string; password: string }) {
// Hash password before storing
const hashedPassword = await this.encryptionService.hashPassword(data.password);
return await this.userRepository.create({
email: data.email,
password: hashedPassword,
});
}
async validatePassword(plainPassword: string, hashedPassword: string) {
return await this.encryptionService.comparePassword(plainPassword, hashedPassword);
}
}Role Management
import { RoleService } from 'najm-auth';
@Service()
export class SetupService {
constructor(private roleService: RoleService) {}
async seedRoles() {
const defaultRoles = [
{ name: 'admin', description: 'Administrator' },
{ name: 'teacher', description: 'Teacher' },
{ name: 'student', description: 'Student' },
];
return await this.roleService.seedDefaultRoles(defaultRoles);
}
}Permission Management
import { PermissionService } from 'najm-auth';
@Service()
export class SetupService {
constructor(private permissionService: PermissionService) {}
async seedPermissions() {
const defaultPermissions = [
{ name: 'read:users', action: 'read', resource: 'users' },
{ name: 'write:users', action: 'write', resource: 'users' },
{ name: 'delete:users', action: 'delete', resource: 'users' },
];
await this.permissionService.seedDefaultPermissions(defaultPermissions);
// Assign permissions to roles
const rolePermissions = [
{
roleName: 'admin',
permissions: ['*:*'], // Admin gets all permissions
},
{
roleName: 'teacher',
permissions: ['read:users', 'read:classes'],
},
];
return await this.permissionService.seedDefaultRolePermissions(rolePermissions);
}
}API Reference
Services
AuthService
loginUser(userId)- Generate tokens for userrefreshTokens()- Refresh access and refresh tokenslogoutUser(userId)- Revoke user's refresh tokengetUserProfile(userData)- Get user profile with language
TokenService
generateTokens(userId)- Generate access and refresh tokensrefreshTokens()- Refresh both tokensverifyAccessToken(token)- Verify access tokenverifyRefreshToken(token)- Verify refresh tokengetUserIdByAccessToken(header)- Extract user ID from tokengetUser(auth)- Get user from access tokengetUserRole(auth)- Get user role from tokengetUserPermissions(auth)- Get user permissions from token
EncryptionService
hashPassword(password)- Hash password with bcryptcomparePassword(password, hash)- Compare password with hash
RoleService
getAll()- Get all rolesgetById(id)- Get role by IDgetByName(name)- Get role by namecreate(data)- Create new roleupdate(id, data)- Update roledelete(id)- Delete roleseedDefaultRoles(roles)- Seed initial roles
PermissionService
getAll()- Get all permissionsgetById(id)- Get permission by IDgetByName(name)- Get permission by namecreate(data)- Create permissionupdate(id, data)- Update permissiondelete(id)- Delete permissionassignPermissionToRole(roleId, permissionId)- Grant permission to roleremovePermissionFromRole(roleId, permissionId)- Revoke permission from roleseedDefaultPermissions(permissions)- Seed initial permissionsseedDefaultRolePermissions(mapping)- Assign permissions to roles
Guards
Role Guards
@isAuth()- Require authentication@Role(...roles)- Require specific role(s)@isAdmin()- Shorthand for admin role@isTeacher()- Shorthand for teacher role@isStudent()- Shorthand for student role@isStaff()- Allow admin, principal, accounting, secretary, teacher
Permission Guards
@Permission(permission)- Require specific permission
Constants
ROLES = {
ADMIN: 'admin',
PRINCIPAL: 'principal',
ACCOUNTING: 'accounting',
SECRETARY: 'secretary',
TEACHER: 'teacher',
STUDENT: 'student',
PARENT: 'parent',
}
ROLE_GROUPS = {
ADMINISTRATORS: ['admin', 'principal'],
FINANCIAL: ['admin', 'accounting'],
STAFF: ['admin', 'principal', 'accounting', 'secretary', 'teacher'],
END_USERS: ['student', 'parent'],
ALL: [...all roles],
}License
MIT
