Authorization & Checks
Security and validation checks
Checks provide authorization and validation for your endpoints - ensuring users can only do what they're allowed to do.
What are Checks?
Checks are reusable authorization/ validation logic:
- Run before handlers execute
- Can access context and args
- Return
AsyncResult<Unit, CheckError> - Can be composed and chained
Creating Checks
import { check } from '@deessejs/functions';
const isAuthenticated = check({
args: z.object({}),
handler: async (args) => {
if (!args.userId) {
return failure(new CheckError("NOT_AUTHENTICATED"));
}
return success(unit);
},
});Using Checks
Apply checks to queries and mutations:
const deleteUser = t.mutation({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
return success(await ctx.db.delete(args.id));
},
})
.before(check(isAuthenticated))
.before(check(isAdmin));Common Checks
Authentication
const authenticated = check({
args: z.object({}),
handler: async (args) => {
if (!args.userId) {
return failure(new CheckError("UNAUTHORIZED", {
message: "You must be logged in",
}));
}
return success(unit);
},
});Role-Based Authorization
const isAdmin = check({
args: z.object({}),
handler: async (ctx) => {
const user = await ctx.db.find(ctx.userId);
if (user.role !== "admin") {
return failure(new CheckError("FORBIDDEN", {
message: "Admin access required",
}));
}
return success(unit);
},
});
const isModerator = check({
args: z.object({}),
handler: async (ctx) => {
const user = await ctx.db.find(ctx.userId);
if (!["admin", "moderator"].includes(user.role)) {
return failure(new CheckError("FORBIDDEN"));
}
return success(unit);
},
});Resource Ownership
const ownsResource = (resourceType: string) => check({
args: z.object({ resourceId: z.number() }),
handler: async (ctx, args) => {
const resource = await ctx.db.find(resourceType, args.resourceId);
if (resource.ownerId !== ctx.userId) {
return failure(new CheckError("FORBIDDEN", {
message: "You don't own this resource",
}));
}
return success(unit);
},
});Permission-Based
const hasPermission = (permission: string) => check({
args: z.object({}),
handler: async (ctx) => {
const user = await ctx.db.find(ctx.userId);
const permissions = await ctx.db.getPermissions(user.roleId);
if (!permissions.includes(permission)) {
return failure(new CheckError("FORBIDDEN", {
message: `Permission '${permission}' required`,
}));
}
return success(unit);
},
});
// Usage
.before(check(hasPermission("users.delete")))Composing Checks
Combine multiple checks:
const canDeleteUser = compose(
isAuthenticated,
isAdmin,
);
const canEditPost = compose(
isAuthenticated,
isModerator,
ownsResource("post"),
);Real-World Example
import { check, compose } from '@deessejs/functions';
import { success, failure, unit } from '@deessejs/functions';
import { CheckError } from '@deessejs/functions/checks';
// Define checks
const authenticated = check({
args: z.object({}),
handler: async (ctx) => {
if (!ctx.userId) {
return failure(new CheckError("UNAUTHORIZED"));
}
return success(unit);
},
});
const isAdmin = check({
args: z.object({}),
handler: async (ctx) => {
const user = await ctx.db.users.find(ctx.userId);
if (user.role !== "admin") {
return failure(new CheckError("FORBIDDEN"));
}
return success(unit);
},
});
const ownsPost = check({
args: z.object({ postId: z.number() }),
handler: async (ctx, args) => {
const post = await ctx.db.posts.find(args.postId);
if (post.authorId !== ctx.userId) {
return failure(new CheckError("FORBIDDEN"));
}
return success(unit);
},
});
// Apply to endpoints
const deleteUser = t.mutation({
args: z.object({ userId: z.number() }),
handler: async (ctx, args) => {
return success(await ctx.db.users.delete(args.userId));
},
})
.before(check(authenticated))
.before(check(isAdmin));
const updatePost = t.mutation({
args: z.object({
postId: z.number(),
title: z.string(),
content: z.string(),
}),
handler: async (ctx, args) => {
return success(await ctx.db.posts.update(args.postId, args));
},
})
.before(check(compose(authenticated, ownsPost)));Best Practices
1. Use Descriptive Check Errors
// ✅ Good - descriptive
return failure(new CheckError("FORBIDDEN", {
message: "You must be the post author to edit",
errorCode: "NOT_AUTHOR",
}));
// ❌ Bad - generic
return failure(new Error("Access denied"));2. Keep Checks Focused
// ✅ Good - single responsibility
const isAuthenticated = check({ /* ... */ });
const isAdmin = check({ /* ... */ });
// ❌ Bad - doing too much
const authAndAdminAndOwner = check({
handler: async (ctx) => {
if (!ctx.userId) return failure(...);
if (user.role !== "admin") return failure(...);
if (!ownsResource()) return failure(...);
},
});3. Reuse Checks
// ✅ Good - reusable
export const checks = {
authenticated,
isAdmin,
isModerator,
ownsResource: (type) => check(/* ... */),
};
// Use across app
.before(check(checks.authenticated))
.before(check(checks.isAdmin))See Also
- Validation - Input validation
- Lifecycle Hooks - beforeInvoke for custom logic