User Context
Zelt provides request-scoped functions to access and manage the authenticated user.
Core Functions
| Function | Description |
|---|---|
setUser(user, roles) | Set the authenticated user (call in middleware) |
currentUser() | Get the current user (returns undefined if not authenticated) |
currentRoles() | Get the current user's roles (returns [] if not authenticated) |
Setting the User
Call setUser() in your authentication middleware after validating credentials:
export const authMiddleware: FunctionMiddleware = async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (token) {
const payload = await verifyToken(token);
setUser(
{ id: payload.sub, name: payload.name, email: payload.email },
payload.roles
);
}
await next();
};
Parameters
- user — Any object representing the authenticated user
- roles — Array of role strings (e.g.,
['admin', 'user'])
Accessing the User
In Route Handlers
Use currentUser() to access the authenticated user:
@Controller('/profile')
class ProfileController {
@Get('/me')
me() {
const user = currentUser();
const roles = currentRoles();
if (!user) {
throw new HTTPException(401, { message: 'Not authenticated' });
}
return { user, roles, isAdmin: roles.includes('admin') };
}
}
With Default Parameters
For cleaner handler signatures, use default parameters:
@Controller('/profile')
class ProfileController {
@Get('/me')
me(user = currentUser()) {
return user;
}
}
Type-Safe User Context
By default, currentUser() returns Record<string, unknown>. Extend RequestContextSchema via declaration merging to get full type safety:
declare module '@zeltjs/core' {
interface RequestContextSchema {
user: {
id: string;
name: string;
email: string;
};
authRoles: ('admin' | 'editor' | 'user')[];
}
}
Now all user-related functions are typed:
import { currentUser, currentRoles, setUser } from '@zeltjs/core';
const user = currentUser();
// TypeScript knows: user?.id, user?.name, user?.email
const roles = currentRoles();
// TypeScript knows: roles is ('admin' | 'editor' | 'user')[]
setUser(
{ id: '123', name: 'Alice', email: 'alice@example.com' },
['admin', 'user']
);
// Type-checked against RequestContextSchema
Where to Put the Type Declaration
Create a types/zelt.d.ts file in your project:
declare module '@zeltjs/core' {
interface RequestContextSchema {
user: {
id: string;
name: string;
email: string;
avatarUrl?: string;
};
authRoles: ('admin' | 'moderator' | 'user')[];
}
}
export {};
Make sure your tsconfig.json includes this file:
{
"include": ["src/**/*", "types/**/*"]
}
User Design Best Practices
Keep It Minimal
Only include fields you need in handlers. Don't copy the entire database record:
// ✅ Good — minimal context
interface RequestContextSchemaGood {
user: {
id: string;
name: string;
};
}
// ❌ Avoid — too much data
interface RequestContextSchemaBad {
user: {
id: string;
name: string;
email: string;
passwordHash: string; // Never include sensitive data
createdAt: Date;
updatedAt: Date;
preferences: object;
// ... 20 more fields
};
}
Fetch Additional Data When Needed
Use the user ID to fetch more data in specific handlers:
@Controller('/settings')
class SettingsController {
constructor(private userRepo = inject(UserRepository)) {}
@Authorized()
@Get('/')
async getSettings() {
const user = currentUser();
if (!user) return;
const fullUser = await this.userRepo.findById(user.id);
return { preferences: fullUser.preferences };
}
}
Consider Role Granularity
Roles should be simple strings. Complex permission logic belongs in services:
// ✅ Good — simple roles
type GoodRoles = ('admin' | 'editor' | 'viewer')[];
// ❌ Avoid — overly specific roles
type BadRoles = ('can_edit_posts' | 'can_delete_posts' | 'can_view_analytics')[];
For fine-grained permissions, check roles in your service layer:
function canEdit(post: Post): boolean {
const user = currentUser();
const roles = currentRoles();
if (roles.includes('admin')) return true;
if (roles.includes('editor') && post.authorId === user?.id) return true;
return false;
}