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:
@Middleware
export class AuthMiddleware {
async use(next: Next, req = request()): Promise<Response | undefined> {
const token = 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();
return undefined;
}
}
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;
}
}
Typed Access
currentUser() always returns Record<string, unknown> | undefined — the shape passed to setUser() isn't tracked at the type level, so reading a specific field requires a manual assertion or narrowing.
For a value with a concrete, narrowed type available to handlers, provide it through a middleware instead: declare it with Next<T> and pass it to next(value), then read it with middlewareValue(M). See Middleware Values for details.
User Design Best Practices
Keep It Minimal
Only include fields you need in handlers. Don't copy the entire database record:
// ✅ Good — minimal user
setUser({ id: '123', name: 'Alice' }, ['user']);
// ❌ Avoid — copying the entire record
setUser({
id: '123',
name: 'Alice',
email: 'alice@example.com',
passwordHash: '...', // Never include sensitive data
createdAt: new Date(),
updatedAt: new Date(),
preferences: {},
// ... 20 more fields
}, ['user']);
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 (!isSessionUser(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') && isSessionUser(user) && post.authorId === user.id) return true;
return false;
}