Skip to content

Commit

Permalink
feat: Implemented role based access control
Browse files Browse the repository at this point in the history
  • Loading branch information
siddiquiaffan committed Aug 17, 2024
1 parent a9336e0 commit 422693f
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 0 deletions.
52 changes: 52 additions & 0 deletions src/components/auth/withPermissionCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
@Author: siddiquiaffan
*/

import React from 'react'
import type { Role } from '@/server/db/schema'
import { checkAccess } from '@/lib/auth/check-access'
import { redirect } from 'next/navigation'

type Options = ({
Fallback: React.FC
}) | ({
redirect: string
})

const DefaultFallback = () => <div>Permission denied</div>

/**
*
* A high order component which takes a component and roles as arguments and returns a new component.
* @example
* ```
* withPermissionCheck(
* MyComponent,
* ['user', 'moderator'],
* { Fallback: () => <div>Permission denied</div> }
* )
* ```
*/

// eslint-disable-next-line
const withPermissionCheck = <T extends Record<string, any>>(Component: React.FC<T>, roles: Role[], options?: Options): React.FC<T> => {

return async (props: T) => {

const hasPermission = await checkAccess(roles)

if (!hasPermission) {
if (options && 'redirect' in options) {
redirect(options.redirect)
} else {
const Fallback = options?.Fallback ?? DefaultFallback
return <Fallback />
}
}

return <Component {...props} />
}
};


export default withPermissionCheck
60 changes: 60 additions & 0 deletions src/lib/auth/check-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
@Author: siddiquiaffan
@Desc: Utility functions for role based access
*/

import { type Role } from "@/server/db/schema";
import { validateRequest } from './validate-request'
import { cache } from "react";

export async function uncachedCheckAccess(
role: Role | Role[],
{
method,
}: {
method: "some" | "every";
} = { method: "some" },
): Promise<boolean> {
const { user } = await validateRequest();
if (!user) {
return false;
}

// admin can access everything
if (user.roles?.includes("admin")) {
return true;
}

if (Array.isArray(role)) {
return role[method]((r: string) => user.roles?.includes(r as Role));
}

return !!user.roles?.includes(role as Role);
}

/**
* Check if the user has access
*/
export const checkAccess = cache(uncachedCheckAccess);


// ============== { Separate methods for each role type } ==============
export async function isModerator(): Promise<boolean> {
return checkAccess("moderator");
}

export async function isAdmin(): Promise<boolean> {
return checkAccess("admin");
}

export async function isContentCreator(): Promise<boolean> {
return checkAccess("content-creator");
}

export async function isOnlyUser(): Promise<boolean> {
const { user } = await validateRequest();
if (!user) {
return false;
}
return user.roles?.length === 1 && user.roles[0] === "user";
}
1 change: 1 addition & 0 deletions src/lib/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const lucia = new Lucia(adapter, {
avatar: attributes.avatar,
createdAt: attributes.createdAt,
updatedAt: attributes.updatedAt,
roles: attributes.roles,
};
},
sessionExpiresIn: new TimeSpan(30, "d"),
Expand Down
5 changes: 5 additions & 0 deletions src/server/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import {
text,
timestamp,
varchar,
json,
} from "drizzle-orm/pg-core";
import { DATABASE_PREFIX as prefix } from "@/lib/constants";

export const pgTable = pgTableCreator((name) => `${prefix}_${name}`);

export type Role = 'user' | 'admin' | 'moderator' | 'content-creator';

export const users = pgTable(
"users",
{
Expand All @@ -27,6 +30,7 @@ export const users = pgTable(
stripeCurrentPeriodEnd: timestamp("stripe_current_period_end"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at", { mode: "date" }).$onUpdate(() => new Date()),
roles: json("roles").$type<Role[]>().default(['user']),
},
(t) => ({
emailIdx: index("user_email_idx").on(t.email),
Expand Down Expand Up @@ -106,3 +110,4 @@ export const postRelations = relations(posts, ({ one }) => ({

export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
export type PostWithUser = Post & { user: User };

0 comments on commit 422693f

Please sign in to comment.