Overview
Ensure users can only access their own resources.
Features
- Generic resource support
- Bypass roles for admins
- Flexible owner field
- 404 handling
Installation
npx hanma add elysia-ownership-guard --framework elysia
Code
import { Elysia } from "elysia";
/**
* User with ID for ownership comparison
*/
export interface OwnerUser {
id: string;
role?: string;
}
/**
* Resource with owner information
*/
export interface OwnedResource {
userId?: string;
ownerId?: string;
createdBy?: string;
}
export interface OwnershipConfig<T extends OwnedResource> {
/** Function to fetch the resource by ID */
getResource: (id: string) => Promise<T | null> | T | null;
/** Field on resource that contains owner ID (default: userId) */
ownerField?: keyof T;
/** Roles that bypass ownership check (default: ["admin", "superadmin"]) */
bypassRoles?: string[];
/** Parameter name containing resource ID (default: id) */
paramName?: string;
}
/**
* Get owner ID from resource
*/
const getOwnerId = <T extends OwnedResource>(
resource: T,
field: keyof T
): string | null => {
const value = resource[field];
return typeof value === "string" ? value : null;
};
/**
* Create ownership guard for a specific resource type
*/
export const createOwnershipGuard = <T extends OwnedResource>(
config: OwnershipConfig<T>
) => {
const ownerField = (config.ownerField ?? "userId") as keyof T;
const bypassRoles = config.bypassRoles ?? ["admin", "superadmin"];
const paramName = config.paramName ?? "id";
return new Elysia({ name: "ownership-guard" })
.derive({ as: "scoped" }, async ({ params }) => {
const resourceId = (params as Record<string, string>)[paramName];
if (!resourceId) {
return { resource: null, resourceExists: false };
}
const resource = await config.getResource(resourceId);
return {
resource,
resourceExists: resource !== null,
};
});
};
/**
* Require resource ownership - use with beforeHandle
*/
export const requireOwnership = <T extends OwnedResource>(
options: {
ownerField?: keyof T;
bypassRoles?: string[];
} = {}
) => {
const ownerField = (options.ownerField ?? "userId") as keyof T;
const bypassRoles = options.bypassRoles ?? ["admin", "superadmin"];
return ({
user,
resource,
resourceExists,
set,
}: {
user: OwnerUser | null;
resource: T | null;
resourceExists: boolean;
set: { status: number };
}) => {
// Must be authenticated
if (!user) {
set.status = 401;
return {
success: false,
error: {
code: "UNAUTHORIZED",
message: "Authentication required",
},
};
}
// Resource must exist
if (!resourceExists || !resource) {
set.status = 404;
return {
success: false,
error: {
code: "NOT_FOUND",
message: "Resource not found",
},
};
}
// Check bypass roles
if (user.role && bypassRoles.includes(user.role)) {
return; // Allow access
}
// Check ownership
const ownerId = getOwnerId(resource, ownerField);
if (ownerId !== user.id) {
set.status = 403;
return {
success: false,
error: {
code: "FORBIDDEN",
message: "You do not have permission to access this resource",
},
};
}
};
};
/**
* Helper to check ownership without blocking
*/
export const isOwner = <T extends OwnedResource>(
user: OwnerUser | null,
resource: T | null,
ownerField: keyof T = "userId" as keyof T
): boolean => {
if (!user || !resource) return false;
const ownerId = getOwnerId(resource, ownerField);
return ownerId === user.id;
};
/* USAGE:
import { Elysia, t } from "elysia";
import { authGuard, requireAuth } from "./guards/auth";
import { createOwnershipGuard, requireOwnership } from "./guards/ownership";
// Example: Post resource
interface Post {
id: string;
userId: string;
title: string;
content: string;
}
// Mock database
const posts = new Map<string, Post>();
// Create ownership guard for posts
const postOwnershipGuard = createOwnershipGuard<Post>({
getResource: (id) => posts.get(id) ?? null,
ownerField: "userId",
paramName: "id",
});
const app = new Elysia()
.use(authGuard)
// Get post (public)
.get("/posts/:id", ({ params }) => {
return posts.get(params.id) ?? null;
})
// Update post (owner only)
.use(postOwnershipGuard)
.put("/posts/:id", ({ resource, body }) => {
// resource is typed as Post
return { ...resource, ...body };
}, {
beforeHandle: [requireAuth, requireOwnership<Post>()],
body: t.Object({
title: t.Optional(t.String()),
content: t.Optional(t.String()),
}),
})
// Delete post (owner or admin)
.delete("/posts/:id", ({ resource }) => {
posts.delete(resource!.id);
return { deleted: true };
}, {
beforeHandle: [
requireAuth,
requireOwnership<Post>({ bypassRoles: ["admin"] }),
],
})
.listen(3000);
*/
Usage
import { createOwnershipGuard, requireOwnership } from './guards/ownership';
const postGuard = createOwnershipGuard({
getResource: (id) => db.posts.find(id),
});
app.use(postGuard)
.put('/posts/:id', () => {}, { beforeHandle: [requireOwnership()] });
Dependencies
No external dependencies required.
Frequently Asked Questions
How do I install Ownership Guard?
Run the following command:
npx hanma add elysia-ownership-guard --framework elysiaHow do I use this snippet?
Copy the code and paste it into your project. The snippet will be saved to
src/guards/ownership.ts.Can I customize this snippet?
Yes! All snippets are copy-paste code that you own. Feel free to modify them to fit your needs.
Does this work with other frameworks?
This snippet is designed for the specific framework. Check the other framework pages for equivalent implementations.