Overview
The POS Nest API uses Supabase for authentication, implementing JWT-based token authentication with role-based access control (RBAC). All routes are protected by default, requiring explicit marking for public access.
Supabase Integration
Authentication is handled through Supabase’s authentication service, which provides:
User registration and login
JWT token generation and validation
Role management via user metadata
Secure password handling
Authentication Service
The AuthService manages user authentication operations:
import {
BadRequestException ,
Inject ,
Injectable ,
UnauthorizedException ,
} from '@nestjs/common' ;
import { SupabaseClient } from '@supabase/supabase-js' ;
@ Injectable ()
export class AuthService {
constructor (
@ Inject ( 'SUPABASE_AUTH' ) private readonly supabase : SupabaseClient ,
) {}
async signUp ( signUpDto : SignUpDto ) {
const { email , password } = signUpDto ;
const { data , error } = await this . supabase . auth . admin . createUser ({
email ,
password ,
app_metadata: { role: 'user' },
email_confirm: true ,
});
if ( error ) {
throw new BadRequestException ( error . message );
}
return {
message: 'User created successfully' ,
user: {
id: data . user . id ,
email: data . user . email ,
role: data . user . app_metadata ?. role ,
},
};
}
async signIn ( signInDto : SignInDto ) {
const { email , password } = signInDto ;
const { data , error } = await this . supabase . auth . signInWithPassword ({
email ,
password ,
});
if ( error ) {
throw new UnauthorizedException ( error . message );
}
return {
message: 'Login successful' ,
user: {
id: data . user . id ,
email: data . user . email ,
role: data . user . app_metadata ?. role ,
},
access_token: data . session . access_token ,
refresh_token: data . session . refresh_token ,
expires_in: data . session . expires_in ,
};
}
}
Users are assigned a role in app_metadata during registration. The default role is user, while admins are created separately.
Role-Based Access Control
The API implements a two-tier role system:
user : Standard users with limited permissions
admin : Administrators with full access
Creating Admin Users
Admin users are created through a dedicated endpoint:
async createAdmin ( createAdminDto : CreateAdminDto ) {
const { email , password } = createAdminDto ;
const { data , error } = await this . supabase . auth . admin . createUser ({
email ,
password ,
app_metadata: { role: 'admin' },
email_confirm: true ,
});
if ( error ) {
throw new BadRequestException ( error . message );
}
return {
message: 'Admin user created successfully' ,
user: {
id: data . user . id ,
email: data . user . email ,
role: data . user . app_metadata ?. role ,
},
};
}
Guards
The application uses two global guards to protect routes:
SupabaseAuthGuard
Validates JWT tokens and attaches user information to requests:
src/auth/guards/supabase-auth.guard.ts
import {
CanActivate ,
ExecutionContext ,
Inject ,
Injectable ,
UnauthorizedException ,
} from '@nestjs/common' ;
import { Reflector } from '@nestjs/core' ;
import { SupabaseClient } from '@supabase/supabase-js' ;
import { IS_PUBLIC_KEY } from '../decorators/public.decorator' ;
@ Injectable ()
export class SupabaseAuthGuard implements CanActivate {
constructor (
@ Inject ( 'SUPABASE_AUTH' ) private readonly supabase : SupabaseClient ,
private readonly reflector : Reflector ,
) {}
async canActivate ( context : ExecutionContext ) : Promise < boolean > {
const isPublic = this . reflector . getAllAndOverride < boolean >( IS_PUBLIC_KEY , [
context . getHandler (),
context . getClass (),
]);
const request = context . switchToHttp (). getRequest ();
const token = this . extractTokenFromHeader ( request );
if ( isPublic && ! token ) {
return true ;
}
if ( ! token ) {
throw new UnauthorizedException ( 'Missing authorization token' );
}
try {
const {
data : { user },
error ,
} = await this . supabase . auth . getUser ( token );
if ( error || ! user ) {
throw new UnauthorizedException ( 'Invalid or expired token' );
}
request . user = {
id: user . id ,
email: user . email ,
role: user . app_metadata ?. role ?? 'user' ,
app_metadata: user . app_metadata ,
user_metadata: user . user_metadata ,
};
return true ;
} catch ( error ) {
if ( error instanceof UnauthorizedException ) {
throw error ;
}
throw new UnauthorizedException ( 'Invalid or expired token' );
}
}
private extractTokenFromHeader ( request : any ) : string | undefined {
const [ type , token ] = request . headers . authorization ?. split ( ' ' ) ?? [];
return type === 'Bearer' ? token : undefined ;
}
}
RolesGuard
Enforces role-based access control:
src/auth/guards/roles.guard.ts
import {
CanActivate ,
ExecutionContext ,
ForbiddenException ,
Injectable ,
} from '@nestjs/common' ;
import { Reflector } from '@nestjs/core' ;
import { ROLES_KEY } from '../decorators/roles.decorator' ;
@ Injectable ()
export class RolesGuard implements CanActivate {
constructor ( private readonly reflector : Reflector ) {}
canActivate ( context : ExecutionContext ) : boolean {
const requiredRoles = this . reflector . getAllAndOverride < string []>(
ROLES_KEY ,
[ context . getHandler (), context . getClass ()],
);
if ( ! requiredRoles || requiredRoles . length === 0 ) {
return true ;
}
const request = context . switchToHttp (). getRequest ();
const user = request . user ;
if ( ! user ) {
throw new ForbiddenException ( 'Access denied' );
}
const hasRole = requiredRoles . includes ( user . role );
if ( ! hasRole ) {
throw new ForbiddenException (
`Access denied. Required role: ${ requiredRoles . join ( ', ' ) } ` ,
);
}
return true ;
}
}
Decorators
Custom decorators simplify authentication and authorization:
@Public()
Marks routes as publicly accessible:
src/auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common' ;
export const IS_PUBLIC_KEY = 'isPublic' ;
export const Public = () => SetMetadata ( IS_PUBLIC_KEY , true );
Usage:
@ Get ()
@ Public ()
findAll () {
return this . productsService . findAll ();
}
@Roles()
Restricts routes to specific roles:
src/auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common' ;
export const ROLES_KEY = 'roles' ;
export const Roles = ( ... roles : string []) => SetMetadata ( ROLES_KEY , roles );
Usage:
@ Post ()
@ Roles ( 'admin' )
create (@ Body () createProductDto : CreateProductDto ) {
return this . productsService . create ( createProductDto );
}
@CurrentUser()
Extracts user information from the request:
src/auth/decorators/current-user.decorator.ts
import { createParamDecorator , ExecutionContext } from '@nestjs/common' ;
export const CurrentUser = createParamDecorator (
( data : string | undefined , ctx : ExecutionContext ) => {
const request = ctx . switchToHttp (). getRequest ();
const user = request . user ;
return data ? user ?.[ data ] : user ;
},
);
Usage:
@ Get ( 'profile' )
getProfile (@ CurrentUser () user : any ) {
return user ;
}
@ Get ( 'email' )
getEmail (@ CurrentUser ( 'email' ) email : string ) {
return { email };
}
Token Management
Include the access token in the Authorization header:
Authorization: Bearer < access_toke n >
Token Flow
Authentication Response
{
"message" : "Login successful" ,
"user" : {
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"email" : "[email protected] " ,
"role" : "user"
},
"access_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"refresh_token" : "v1.MRjsZYy4ZXEuLTExMS00MDQ5LWJh..." ,
"expires_in" : 3600
}
Route Protection Examples
Public Routes
Accessible without authentication:
@ Controller ( 'products' )
export class ProductsController {
@ Get ()
@ Public ()
findAll () {
// Anyone can access
return this . productsService . findAll ();
}
@ Get ( ':id' )
@ Public ()
findOne (@ Param ( 'id' ) id : string ) {
// Anyone can access
return this . productsService . findOne ( + id );
}
}
Protected Routes
Require authentication (default behavior):
@ Get ( 'profile' )
getProfile (@ CurrentUser () user : any ) {
// Requires valid token
return user ;
}
Admin-Only Routes
Require admin role:
@ Post ()
@ Roles ( 'admin' )
create (@ Body () createProductDto : CreateProductDto ) {
// Only admins can access
return this . productsService . create ( createProductDto );
}
@ Delete ( ':id' )
@ Roles ( 'admin' )
remove (@ Param ( 'id' ) id : string ) {
// Only admins can access
return this . productsService . remove ( + id );
}
Security Best Practices
Never expose your Supabase service role key in client-side code. Use environment variables for all sensitive credentials.
Strong Passwords Enforce minimum 6-character passwords using validation
Token Validation All tokens validated against Supabase on each request
Role Enforcement Roles checked at the guard level before controller execution
HTTPS Only Always use HTTPS in production environments
Architecture Learn about the application structure
Error Handling Handle authentication errors
Auth Endpoints API reference for authentication