Functions

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

On this page