Skip to content

Commit

Permalink
The one with permissions - v1.0.3 #183
Browse files Browse the repository at this point in the history
* Permissions against admin users for various tabs
* Env changes and better usage on emails etc
* Header loads less on client
*  Attendee list stats is real and times are working
  • Loading branch information
bardsley authored Sep 24, 2024
2 parents 0d4748f + f51afce commit a6f2672
Show file tree
Hide file tree
Showing 27 changed files with 651 additions and 515 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
.env.local
.env.production
.idea

desktop.ini
.vercel

sendgrid.env
Expand Down
13 changes: 13 additions & 0 deletions app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";
import AuthCheck from "@components/admin/authCheck";

export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return <AuthCheck>
{children}
</AuthCheck>

}
2 changes: 0 additions & 2 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ export default async function AdminDashboardPage() {
<h1 className="text-2xl md:text-5xl px-4 ">Admin dashboard</h1>
<UserButton showName={true}/>
</div>


<Hub></Hub>
</SignedIn>
<SignedOut>
Expand Down
9 changes: 9 additions & 0 deletions app/admin/scan/scan-client.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
'use client'
import { useUser } from "@clerk/clerk-react";
import { authUsage } from "@lib/authorise";
import QrReader from "@components/admin/scan/QRReader"
import { useEffect, useState } from "react"
import ScanSuccessDialog from "@components/admin/scan/ScanSuccessDialog"
Expand All @@ -7,14 +9,21 @@ import ScanSuccessDialog from "@components/admin/scan/ScanSuccessDialog"
const ScanClient = () => {
const [scannedResult, setScannedResult] = useState<string>('')
const [scannerActive, setScannerActive] = useState<boolean>(true)
const { user, isLoaded } = useUser();

useEffect(() => {
console.log("Client noticed change")
if (scannedResult) {
setScannerActive(false)
}
}, [scannedResult])

const debug = true

if (!isLoaded) { return <div>Loading</div> }
if (!user) { return <div>Not logged in</div> }
if(!authUsage(user, "/admin/scan")) { return <div>Not authorised</div> }

return (
<div className="max-w-screen-sm mx-auto ">
{debug
Expand Down
11 changes: 2 additions & 9 deletions app/admin/ticketing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Layout from "@components/layout/layout";
import { Container } from "@components/layout/container";
import Navigation from "@components/admin/navigation";
import TicketList from "@components/admin/ticketList";
import StatBlock, {StatLine} from "@components/admin/statBlock";
import AttendeeStats from "@components/admin/attendeeStats";
import { admin_ticketing_url } from "@lib/urls";

export default async function AdminDashboardPage() {
Expand All @@ -13,13 +13,6 @@ export default async function AdminDashboardPage() {
{ name: 'Ticketing', href: admin_ticketing_url, current: true },
]

const stats = [
{ name: 'Total', value: '405', unit: 'tickets' },
{ name: 'Today', value: '10', unit: 'tickets' },
{ name: 'Meal prefs', value: '3', unit: 'set' },
{ name: 'Dinner Prefs', value: '2.5%', unit: 'complete' },
] as StatLine[]

return (<Layout>
<section className={`flex-1 relative transition duration-150 ease-out body-font overflow-hidden bg-none text-white`}>
{" "}
Expand All @@ -32,7 +25,7 @@ export default async function AdminDashboardPage() {
<p className="">See recent sales, mark as attended and give pass etc</p>
</Container>
<Container width="large" padding="tight" className={`flex-1 pb-2`} size="none">
<StatBlock stats={stats}></StatBlock>
<AttendeeStats/>
</Container>

<Container width="large" padding="tight" className={`flex-1 pb-2`} size="none">
Expand Down
6 changes: 3 additions & 3 deletions app/api/admin/attendees/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createClerkClient } from '@clerk/backend';
import { auth } from '@clerk/nextjs/server';
import { NextRequest } from 'next/server';
import { currentUser } from '@clerk/nextjs/server';
import { guaranteeISOstringFromDate } from '@lib/useful';

export async function GET() {
const {userId} = auth();
Expand Down Expand Up @@ -32,13 +33,12 @@ export async function GET() {
const name_changed = attendee.transferred && attendee.transferred.ticket_number == attendee.ticket_number
const transferred_in = !name_changed && attendee.history
// console.log(attendee.full_name,attendee.ticket_number,attendee.transferred?.ticket_number, attendee.transferred?.ticket_number != attendee.ticket_number)

return {
name: attendee.full_name,
email: attendee.email,
checkin_at: attendee.ticket_used,
passes: attendee.line_items.map((item) => item.description),
purchased_at: new Date(parseInt(attendee.purchase_date) * 1000).toISOString(),
purchased_date: guaranteeISOstringFromDate(attendee.purchase_date),
ticket_number: attendee.ticket_number,
active: attendee.active,
status: attendee.status,
Expand All @@ -48,7 +48,7 @@ export async function GET() {
name_changed: name_changed,
transferred: attendee.transferred,
history: attendee.history,

meal_preferences: attendee.meal_preferences,
};
})

Expand Down
5 changes: 3 additions & 2 deletions app/api/preferences/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ export async function POST(request: Request) {
const data = await request.formData()
const ticket = cookies().get('ticket')
const email = cookies().get('email')
// console.log(data)
console.log(data)
const courseInfo = Array.from(data.entries()).filter((item)=>{ return /course/.test(item[0]) ? true : false }).map((item)=>{ return parseInt(item[1].toString()) })
const dietChoices = Array.from(data.entries()).filter((item)=>{ return /selected\[.*\]/.test(item[0]) ? true : false }).map((item)=>{ return item[0].replace('selected[','').replace(']','') })
console.log("courseInfo", courseInfo)
console.log("Diet",dietChoices)
const apiRequestBody = {
ticket_number: ticket.value,
Expand All @@ -19,7 +20,7 @@ export async function POST(request: Request) {
selected: [...dietChoices],
other: data.get('other'),
},
seating_preference: data.get('seating_preference').toString().split(',').filter((item)=>{ return item.length > 0}),
seating_preference: data.get('seating_preference') ? data.get('seating_preference').toString().split(',').filter((item)=>{ return item.length > 0}) : [],
}
}
console.log("POST -> Conor: ",apiRequestBody)
Expand Down
36 changes: 36 additions & 0 deletions components/admin/attendeeStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client'
import useSWR from "swr";
import { fetcher } from "@lib/fetchers";
import StatBlock, { StatLine } from "@components/admin/statBlock";
import { subWeeks } from 'date-fns'
import { guaranteeTimestampFromDate } from '@lib/useful';


export default function AttendeeStats() {
const {data, error, isLoading} = useSWR("/api/admin/attendees", fetcher, { keepPreviousData: false });

const stats = isLoading ?
[
{ name: 'Total', value: 'Loading...', unit: '' },
{ name: 'Today', value: 'Loading...', unit: '' },
{ name: 'Meal prefs', value: 'Loading...', unit: '' },
{ name: 'Dinner Prefs', value: 'Loading...', unit: '' }
] as StatLine[]
: error ? [
{ name: 'Total', value: "Error", unit: '' },
{ name: 'Today', value: 'Error', unit: '' },
{ name: 'Meal prefs', value: 'Error', unit: '' },
{ name: 'Dinner Prefs', value: 'Error', unit: '' }
] as StatLine[]
: [
{ name: 'Total', value: data.attendees.length, unit: 'tickets' },
{ name: 'This week', value: data.attendees.filter((row)=>{
return guaranteeTimestampFromDate(row.purchased_date) > subWeeks(new Date(),1).getTime()
}).length, unit: 'tickets' },
{ name: 'Meal prefs', value: data.attendees.filter((row)=>{return row.meal_preferences}).length, unit: 'set' },
{ name: 'Dinner Prefs', value: 'Unknown', unit: '' }
] as StatLine[]

return <StatBlock stats={stats}></StatBlock>
// return <div>{JSON.stringify(stats,null,2)}</div>
}
19 changes: 19 additions & 0 deletions components/admin/authCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client'
import { useUser } from "@clerk/clerk-react";
import { authUsage } from "@lib/authorise";
import { usePathname } from 'next/navigation'
import { BiSolidHand } from "react-icons/bi";

const containerClasses = "flex w-full h-screen justify-center items-center"
const alertClasses = " border border-1 border-gray-500 rounded-md p-6 flex gap-3 items-center justify-center "

export default function AuthCheck({children}) {
const { user, isLoaded } = useUser();
const path = usePathname()

if(path == '/admin') return children
if (!isLoaded) { return <div className={containerClasses}><div className={alertClasses}>Permission Checking...</div></div> }
if (!user) { return <div className={containerClasses}><div className={alertClasses}>Not Logged.</div></div> }
if(!authUsage(user, path)) { return <div className={containerClasses }><div className={alertClasses + ` text-chillired-800 border-chillired-800`}><BiSolidHand/>Not Authorised!</div></div> }
return children
}
6 changes: 3 additions & 3 deletions components/admin/hub.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'
import { useUser } from "@clerk/clerk-react";
import { authUsage } from "@lib/authorise";
import { admin_ticketing_url } from "@lib/urls";

import {
Expand Down Expand Up @@ -94,11 +95,10 @@ function classNames(...classes) {
export default function Hub() {
const { user, isLoaded } = useUser();
if (!isLoaded) { return <div>Loading</div> }
if (!user) { return <div>Not logged in</div> }

if (!user) { return <div>Not logged in</div> }
return user.publicMetadata.admin ? (
<div className="divide-x divide-richblack-700 border border-richblack-700 overflow-hidden rounded-lg shadow sm:grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 sm:gap-px text-white mt-6">
{actions.map((action, actionIdx) => (
{actions.filter((action) => authUsage(user,action.href)).map((action, actionIdx) => (
<div
key={action.title}
className={classNames( //TODO this is an absolute shitshow and need better working, shame on tailwind components
Expand Down
3 changes: 2 additions & 1 deletion components/admin/lists/ticketRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const TicketRow = ({attendee,setActiveTicket, setNameChangeModalActive, s
<td className="w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium sm:w-auto sm:max-w-none sm:pl-2 vertical-align-top">
<Link href={`/admin/ticketing/ticket/${attendee.ticket_number}/${attendee.email}`} className={`${attendee.active ? 'text-chillired-600 hover:text-chillired-700' : "text-gray-600"}`}>
<span className="text-lg leading-6 sm:text-base md:text-base">{attendee.name}</span><br/>
<span className="text-xs leading-6 text-gray-300">#{attendee.ticket_number}</span>
<span className="text-xs leading-6 text-gray-300">#{attendee.ticket_number}</span><br/>
<span className="text-xs leading-6 text-gray-300">{format(Date.parse(attendee.purchased_date),'h:mmaaa EEE do LLL yyyy')}</span>
</Link>
<dl className="font-normal lg:hidden text-inherit">
<dt className="sr-only">Email</dt>
Expand Down
1 change: 0 additions & 1 deletion components/admin/statBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export type StatLine = {
export default function StatBlock({stats}: {stats: StatLine[]}) {
const [hidden,setHidden] = useState(false);


const toggelButton = <div className="w-full flex absolute -top-12 right-2 justify-end"><button className="border border-gray-600 text-gray-400 hover:text-gray-50 hover:border-gray-100 rounded-md px-3 pt-1 mt-3 text-xs" onClick={()=>{setHidden(!hidden)}}>{ hidden ? "Show" : "Hide"} Stats</button></div>
const statBlock = (<div className="mx-auto max-w-7xl bg-richblack-500 rounded-lg">
<div className="grid gap-px bg-red/5 grid-cols-2 md:grid-cols-4">
Expand Down
5 changes: 3 additions & 2 deletions components/admin/ticket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Link from "next/link";
import NameChangeModal from './modals/nameChangeModal';
import TicketTransferModal from './modals/ticketTransferModal';
import { fetcher } from "@lib/fetchers";
import { guaranteeTimestampFromDate } from "@lib/useful";

const accessToThings = (access:number[],) => {
let products = []
Expand Down Expand Up @@ -52,7 +53,7 @@ export default function TicketView({ticket_number, email}: {ticket_number: strin
if(ticket) {

const ticketUsage = ticket.active ? ticket.ticket_used ? `Used @ ${format(ticket.ticket_used,' eee')}` : "Active & Unused" : "Deactivated"
const purchaseData = fromUnixTime(ticket.purchase_date)
const purchaseDate = fromUnixTime(guaranteeTimestampFromDate(ticket.purchase_date) / 1000)
const purchasedThings = ticket.line_items ? ticket.line_items.map((item) => {return `${item.description} £${item.amount_total / 100}`}): []

return ( data && <div className="w-full md:flex justify-center items-start gap-3 max-w-full">
Expand Down Expand Up @@ -104,7 +105,7 @@ export default function TicketView({ticket_number, email}: {ticket_number: strin
<div className="rounded-lg shadow-lg bg-richblack-600 border-gray-500 border my-4">
<h3 className="font-bold uppercase border-b border-gray-500 py-2 px-4">Purchase</h3>
<div className="p-4">
<Info label="Purchased" info={format(purchaseData,'HH:mm do MMMM yyyy ')} options={{size: 'xl'}}/>
<Info label="Purchased" info={format(purchaseDate,'HH:mm do MMMM yyyy ')} options={{size: 'xl'}}/>
<Info label="Bought" info={purchasedThings} options={{size: 'lg'}} />
{ ticket.promo_code ? <Info label="Promo Code" info={ticket.promo_code} /> : null }
<Info label="Payment Method" info={ticket.status.replace('paid_','')} options={{size: '2xl'}} />
Expand Down
7 changes: 5 additions & 2 deletions components/admin/ticketList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client'

import { useSearchParams, useRouter, usePathname} from 'next/navigation'
import { useState} from 'react';
import { ChevronDownIcon, ChevronUpIcon, ChevronUpDownIcon } from '@heroicons/react/24/solid'
Expand All @@ -10,7 +11,6 @@ import { TicketRow } from './lists/ticketRow';
import { fetcher } from "@lib/fetchers";

export default function TicketList() {

const searchParams = useSearchParams()
const pathname = usePathname()
const router = useRouter()
Expand Down Expand Up @@ -74,6 +74,8 @@ export default function TicketList() {
const headerContainerClassNames = "flex justify-between"
const labelClassNames = "py-3.5 pl-4 block"



if(isLoading) { return <p>Loading...</p> }
// else if (isValidating) { return <p>Validating...plz</p>}
else if (error) { return <p>Error on fetch {JSON.stringify(error)}</p> }
Expand All @@ -83,6 +85,7 @@ export default function TicketList() {
else {

const sortedAttendees = attendees.sort((a, b) => {
// console.log("Sorting",sortByField,a,b)
if (sortByDirection === 'desc') {
return (0 - (a[sortByField] > b[sortByField] ? 1 : -1))
} else {
Expand Down Expand Up @@ -118,7 +121,7 @@ export default function TicketList() {
<FilterLabel fieldname={"name"} addFilterFunction={addFilter}>
<span className={`${labelClassNames} sm:pl-2 `}>Name
<span className='sm:hidden'> & Details</span>
<span className='hidden sm:inline lg:hidden'>& Email</span>
<span className='sm:inline lg:hidden'>& Email</span>
</span>
</FilterLabel>

Expand Down
9 changes: 7 additions & 2 deletions components/admin/userList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function UserList({loggedInUser}) {
console.log("person:",person)
const admin = person.metadata?.public?.admin;
const title = person.metadata?.public?.title;
const roles = person.metadata?.public?.roles || [];
const isMe = person.id === loggedInUser
return (<li
key={person.email}
Expand All @@ -46,14 +47,18 @@ export default function UserList({loggedInUser}) {
<dl className="mt-1 flex flex-grow flex-col justify-between">
<dt className="sr-only">Title</dt>
<dd className="text-sm text-gray-500">{title}</dd>
<dt className="sr-only">Role</dt>
<dt className="sr-only">User Type</dt>
<dd className="mt-3">
<span className={`inline-flex items-center rounded-full
${ admin ? "bg-green-50 text-green-700 ring-green-600/20" : "bg-blue-50 text-blue-700 ring-blue-600/20"}
px-3 pb-0 pt-1 text-xs font-medium ring-1 ring-inset `}>
px-3 pb-1 pt-1 text-xs font-medium ml-3 ring-1 ring-inset `}>
{admin ? "Admin" : "User"}
</span>
</dd>
<dt className="sr-only">Roles</dt>
<dd className="mt-3 text-sm text-gray-500">
{roles.join(", ")}
</dd>
</dl>
</div>
<div>
Expand Down
2 changes: 1 addition & 1 deletion components/layout/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default async function Layout({ children, rawPageData }: LayoutProps) {

return (
<LayoutProvider globalSettings={globalData.global} pageData={rawPageData}>
<Header />
<Header header={globalData.global.header} theme={globalData.global.theme} />
<main
className={cn(
"font-sans flex-1 text-gray-800 bg-gradient-to-br from-white to-gray-50 dark:from-richblack-500 dark:to-richblack-600 flex flex-col"
Expand Down
Loading

0 comments on commit a6f2672

Please sign in to comment.