Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 };