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
7 changes: 4 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<div align="center">
<img src="/docs/img/cover.svg" alt="HKRecruitment">

![](https://img.shields.io/badge/HKN_Chapter-MuNu-blue) ![GitHub commit activity](https://img.shields.io/github/commit-activity/w/MuNuChapterHKN/HKrecruitment) ![GitHub contributors](https://img.shields.io/github/contributors/MuNuChapterHKN/HKrecruitment)
Comment thread
cristiansap marked this conversation as resolved.
![](https://img.shields.io/badge/HKN_Chapter-MuNu-blue) ![GitHub commit activity](https://img.shields.io/github/commit-activity/w/MuNuChapterHKN/HKrecruitment) ![GitHub contributors](https://img.shields.io/github/contributors/MuNuChapterHKN/HKrecruitment)

</div>

A recruitment platform used by the Mu Nu Chapter of Eta Kappa Nu, check out our website to learn more about our chapter [hknpolito.org](https://hknpolito.org).
Expand All @@ -26,9 +27,9 @@ In order to manage multiple versions of node.js and pnpm, I'd suggest using [asd
4. Push the migrations to the database with `pnpm db:push`;
5. Start the development server with `pnpm dev`;
6. Go to `https://localhost:3000/recruitment/dashboard` and log in with your Google account;
7. (Optional) If you want to seed the database, you can use `pnpm db:seed`. You can also set your account as admin by passing `pnpm db:seed --admin-email=your.email@hknpolito.org`.
7. (Optional) If you want to seed the database, you can use `pnpm db:seed`. You can also set your account as admin by passing `pnpm db:seed --admin-email=your.email@hknpolito.org`. For the session-based dashboard roles, you can assign yourself a role for the latest recruitment session with `pnpm db:seed --session-role-email=your.email@hknpolito.org --session-role=guest|member|clerk|admin`, or target a specific session with `--session-id=<session-id>`.

### Development
### Development

Whenever you make any change to the codebase, try to follow these guidelines:

Expand Down
9 changes: 9 additions & 0 deletions src/app/dashboard/[rid]/AdminOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function AdminOverview() {
return (
<div className="flex h-full items-center justify-center p-6">
<h1 className="text-2xl font-semibold text-muted-foreground">
Page accessible from Admins
</h1>
</div>
);
}
9 changes: 9 additions & 0 deletions src/app/dashboard/[rid]/ClerkOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function ClerkOverview() {
return (
<div className="flex h-full items-center justify-center p-6">
<h1 className="text-2xl font-semibold text-muted-foreground">
Page accessible from Clerks
</h1>
</div>
);
}
9 changes: 9 additions & 0 deletions src/app/dashboard/[rid]/GuestOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function GuestOverview() {
return (
<div className="flex h-full items-center justify-center p-6">
<h1 className="text-2xl font-semibold text-muted-foreground">
Page accessible from Guests
</h1>
</div>
);
}
9 changes: 9 additions & 0 deletions src/app/dashboard/[rid]/MemberOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function MemberOverview() {
return (
<div className="flex h-full items-center justify-center p-6">
<h1 className="text-2xl font-semibold text-muted-foreground">
Page accessible from Members
</h1>
</div>
);
}
9 changes: 2 additions & 7 deletions src/app/dashboard/[rid]/RecruitmentSwitcher.tsx
Comment thread
pasc4le marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { RecruitingSession } from '@/db/types';
import { ChevronsUpDown, Plus } from 'lucide-react';
import Link from 'next/link';
import { useRouter, usePathname } from 'next/navigation';
import { useRouter } from 'next/navigation';

export type RecruitmentSwitcherProps = {
selected: RecruitingSession;
Expand All @@ -31,14 +31,9 @@ export default function RecruitmentSwitcher({
}: RecruitmentSwitcherProps) {
const { isMobile } = useSidebar();
const router = useRouter();
const pathname = usePathname();

const switchTo = (option: RecruitmentSwitcherProps['options'][number]) => {
const segments = pathname.split('/');
segments[2] = option.id;
const newPath = segments.join('/');

router.push(newPath);
router.push(`/dashboard/${option.id}`);
};

return (
Expand Down
26 changes: 13 additions & 13 deletions src/app/dashboard/[rid]/Sidebar.data.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
import { A } from '@/lib/abilities';
import { AuthUserRole } from '@/lib/auth';
import type { AppSubject } from '@/lib/abilities';
import { Calendar, CalendarClock, Gauge, Users } from 'lucide-react';

export const LINKS: Record<
string,
A<{
links: A<{ label: string; href: string; icon?: React.ReactNode }>[];
}>
> = {
type SidebarLink = {
label: string;
href: string;
icon?: React.ReactNode;
subject: AppSubject;
};

export const LINKS: Record<string, { links: SidebarLink[] }> = {
platform: {
canRead: AuthUserRole.Guest,
links: [
{
label: 'Overview',
href: '/',
icon: <Gauge />,
canRead: AuthUserRole.Guest,
subject: 'DashboardOverviewPage',
},
{
label: 'Candidates',
href: '/candidates',
icon: <Users />,
canRead: AuthUserRole.Guest,
subject: 'CandidatesPage',
},
{
label: 'Availability Overview',
href: '/availability',
icon: <CalendarClock />,
canRead: AuthUserRole.Guest,
subject: 'AvailabilityOverviewPage',
},
{
label: 'My Availability',
href: '/me/availability',
icon: <Calendar />,
canRead: AuthUserRole.Guest,
subject: 'MyAvailabilityPage',
},
],
},
Expand Down
58 changes: 29 additions & 29 deletions src/app/dashboard/[rid]/Sidebar.tsx
Comment thread
cristiansap marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -71,39 +71,39 @@ export function DashboardSidebar({ user, recruitment }: DashboardSidebarProps) {
</SidebarHeader>
<SidebarContent>
{Object.entries(LINKS).map(([groupName, group], i) => (
<Can I="read" this={group} key={i}>
<SidebarGroup>
<SidebarGroupLabel>{capitalize(groupName)}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{group.links.map((link, j) => (
<Can I="read" this={link} key={j}>
<SidebarMenuItem key={link.label}>
<SidebarMenuButton asChild>
<DashboardLink href={link.href}>
{link.icon}
<span>{link.label}</span>
</DashboardLink>
</SidebarMenuButton>
</SidebarMenuItem>
</Can>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</Can>
<SidebarGroup key={i}>
<SidebarGroupLabel>{capitalize(groupName)}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{group.links.map((link, j) => (
<Can I="read" a={link.subject} key={j}>
<SidebarMenuItem key={link.label}>
<SidebarMenuButton asChild>
<DashboardLink href={link.href}>
{link.icon}
<span>{link.label}</span>
</DashboardLink>
</SidebarMenuButton>
</SidebarMenuItem>
</Can>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<DashboardLink href={'/users'}>
<Users />
<span>Members</span>
</DashboardLink>
</SidebarMenuButton>
</SidebarMenuItem>
<Can I="read" a="MembersPage">
<SidebarMenuItem>
<SidebarMenuButton asChild>
<DashboardLink href={'/members'}>
<Users />
<span>Members</span>
</DashboardLink>
</SidebarMenuButton>
</SidebarMenuItem>
</Can>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand Down
11 changes: 11 additions & 0 deletions src/app/dashboard/[rid]/availability/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { AggregatedAvailabilityTable } from './AggregatedAvailabilityTable';
import { findWithAggregatedAvailability } from '@/lib/services/timeslots';
import { findOne } from '@/lib/services/recruitmentSessions';
import { notFound } from 'next/navigation';
import { auth } from '@/lib/server/auth';
import { headers } from 'next/headers';
import { requirePageAccess } from '@/lib/helpers/pageAuthorization';

export type TimeslotWithAvailability = {
id: string;
Expand All @@ -22,7 +25,15 @@ export type TimeslotWithAvailability = {
export default async function AvailabilityOverviewPage({
params,
}: PageProps<'/dashboard/[rid]/availability'>) {
const session = await auth.api.getSession({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be better to move the page checks to the middleware? Or dashboard layout?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd not move requirePageAccess to the middleware or layout because it queries the DB with both the userId and the recruitingSessionId (rid) to get the role for that specific session (and it's easy to get the rid from this page). So I'd leave it there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recruitmentSessionId is also retrievable from the dashboard/[rid]/layout.tsx.

@cristiansap cristiansap May 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but requirePageAccess also takes a page-specific subject (e.g. 'AvailabilityOverviewPage') as argument. However, the layout doesn't know which page it's currently rendering, so it can't know which subject to pass to requirePageAccess. Moving it there would mean either calling it for every possible subject (which makes no sense) or refactoring the permission granularity entirely.

headers: await headers(),
});
if (!session) return null;

const { rid } = await params;

await requirePageAccess(session.user.id, rid, 'AvailabilityOverviewPage');

const recruitmentSession = await findOne(rid);
if (!recruitmentSession) notFound();

Expand Down
13 changes: 12 additions & 1 deletion src/app/dashboard/[rid]/candidates/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,22 @@ import { findAvailableForBooking } from '@/lib/services/timeslots';
import { revalidatePath } from 'next/cache';
import { INTERVIEW_BOOKING_STAGE } from '@/lib/stages';
import { UpdateFileDialog } from './UpdateFileDialog';
import { auth } from '@/lib/server/auth';
import { headers } from 'next/headers';
import { requirePageAccess } from '@/lib/helpers/pageAuthorization';

export default async function CandidateDetailsPage({
params,
}: PageProps<'/dashboard/[rid]/candidates/[id]'>) {
const { id } = await params;
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) return null;

const { id, rid } = await params;

await requirePageAccess(session.user.id, rid, 'CandidateDetailsPage');

const applicant = await getApplicantById(id);

if (!applicant) {
Expand Down
11 changes: 11 additions & 0 deletions src/app/dashboard/[rid]/candidates/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@ import { getStageLabel, getStageColor } from '@/lib/stages';
import { notFound } from 'next/navigation';
import { findOne } from '@/lib/services/recruitmentSessions';
import { getDegreeLabel } from '@/lib/degrees';
import { auth } from '@/lib/server/auth';
import { headers } from 'next/headers';
import { requirePageAccess } from '@/lib/helpers/pageAuthorization';

export default async function CandidatesPage({
params,
}: PageProps<'/dashboard/[rid]/candidates'>) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) return null;

const { rid } = await params;

await requirePageAccess(session.user.id, rid, 'CandidatesPage');

const recruitmentSession = await findOne(rid);
if (!recruitmentSession) notFound();

Expand Down
18 changes: 15 additions & 3 deletions src/app/dashboard/[rid]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
import { DashboardSidebar } from './Sidebar';
import { auth } from '@/lib/server/auth';
import { headers } from 'next/headers';
import { findAllAsOptions, findOne } from '@/lib/services/recruitmentSessions';
import {
findAllAsOptions,
findOne,
findUserRoleForSession,
} from '@/lib/services/recruitmentSessions';
import { notFound } from 'next/navigation';

type DashboardLayoutProps = {
Expand Down Expand Up @@ -40,11 +44,19 @@ export default async function DashboardLayout({
/* Get All Recruitments as Options for Sidebar */
const recruitmentOptions = await findAllAsOptions();

/* Resolve user role for this recruitment session (if any) */
const sessionRole = await findUserRoleForSession(user.id, rid);

const userForAbility = {
...user,
role: sessionRole,
};

return (
<SidebarProvider>
<AbilityProvider user={user}>
<AbilityProvider user={userForAbility}>
<DashboardSidebar
user={user}
user={userForAbility}
recruitment={{ selected: recruitment, options: recruitmentOptions }}
/>
<main className="w-full">
Expand Down
2 changes: 1 addition & 1 deletion src/app/dashboard/[rid]/me/availability/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default async function AvailabilityPage({
Interview availability
</h1>
<p className="mt-1 text-sm text-gray-500">
Seleziona le tue disponibilità settimanali per le interview.
Select your weekly availability for interviews.
</p>
</header>

Expand Down
Loading