add aime card component
This commit is contained in:
parent
d961ee9a4b
commit
c8a3bf7038
9
package-lock.json
generated
9
package-lock.json
generated
@ -27,6 +27,7 @@
|
||||
"react": "^18",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "^18",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"sass": "^1.71.1",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
@ -9582,6 +9583,14 @@
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz",
|
||||
"integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
@ -33,6 +33,7 @@
|
||||
"react": "^18",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "^18",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"sass": "^1.71.1",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
|
@ -6,15 +6,30 @@ import { auth, signIn, signOut } from '@/auth';
|
||||
import { AuthError } from 'next-auth';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { db } from '@/db';
|
||||
import { UserPermissions } from '@/types/permissions';
|
||||
import { requirePermission } from '@/helpers/permissions';
|
||||
import { UserPayload } from '@/types/user';
|
||||
|
||||
export const getUser = async () => {
|
||||
const session = await auth();
|
||||
return session?.user;
|
||||
};
|
||||
|
||||
export const requireUser = async () => {
|
||||
return await getUser() ??
|
||||
redirect(`/auth/login?error=1&callbackUrl=${encodeURIComponent(new URL(headers().get('referer') ?? 'http://a/').pathname)}`);
|
||||
type RequireUserOptions = {
|
||||
permission?: UserPermissions[] | UserPermissions
|
||||
};
|
||||
|
||||
export const requireUser = async (opts?: RequireUserOptions): Promise<UserPayload> => {
|
||||
const user = await getUser();
|
||||
|
||||
if (!user) {
|
||||
return redirect(`/auth/login?error=1&callbackUrl=${encodeURIComponent(headers().get('x-path') ?? '/')}`)
|
||||
}
|
||||
|
||||
if (opts?.permission !== undefined)
|
||||
requirePermission(user.permissions, ...(Array.isArray(opts.permission) ? opts.permission : [opts.permission]));
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
type LoginOptions = {
|
||||
|
41
src/actions/card.ts
Normal file
41
src/actions/card.ts
Normal file
@ -0,0 +1,41 @@
|
||||
'use server';
|
||||
|
||||
import { requireUser } from '@/actions/auth';
|
||||
import { db } from '@/db';
|
||||
import { UserPermissions } from '@/types/permissions';
|
||||
import { requirePermission } from '@/helpers/permissions';
|
||||
|
||||
export const getCards = async () => {
|
||||
const user = await requireUser();
|
||||
|
||||
return db.selectFrom('aime_card')
|
||||
.where('user', '=', user.id)
|
||||
.selectAll()
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const banUnbanCard = async (opts: { cardId: number, userId: number, isBan: boolean }) => {
|
||||
await requireUser({ permission: UserPermissions.USERMOD });
|
||||
|
||||
await db.updateTable('aime_card')
|
||||
.set({ is_banned: +opts.isBan })
|
||||
.where(({ and, eb }) => and([
|
||||
eb('id', '=', opts.cardId),
|
||||
eb('user', '=', opts.userId)
|
||||
]))
|
||||
.executeTakeFirst();
|
||||
};
|
||||
|
||||
export const lockUnlockCard = async (opts: { cardId: number, userId: number, isLock: boolean }) => {
|
||||
const user = await requireUser();
|
||||
if (opts.userId !== user.id)
|
||||
requirePermission(user.permissions, UserPermissions.USERMOD);
|
||||
|
||||
await db.updateTable('aime_card')
|
||||
.set({ is_locked: +opts.isLock })
|
||||
.where(({ and, eb }) => and([
|
||||
eb('id', '=', opts.cardId),
|
||||
eb('user', '=', opts.userId)
|
||||
]))
|
||||
.executeTakeFirst();
|
||||
};
|
@ -1,3 +1,20 @@
|
||||
export default function SettingsPage() {
|
||||
return (<div>settings</div>);
|
||||
import { getCards } from '@/actions/card';
|
||||
import { Divider } from '@nextui-org/react';
|
||||
import { AimeCard } from '@/components/aime-card';
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const card = await getCards();
|
||||
|
||||
|
||||
return (<div className="w-full flex items-center justify-center">
|
||||
<div className="w-full max-w-full sm:max-w-5xl flex flex-col">
|
||||
<div className="w-full rounded-lg sm:bg-content1 sm:shadow-lg">
|
||||
<div className="text-2xl font-semibold p-4">Cards</div>
|
||||
<Divider className="mb-4 hidden sm:block" />
|
||||
<div className="px-1 sm:px-4 sm:pb-4 flex flex-wrap items-center justify-center gap-4">
|
||||
{card.map(c => <AimeCard key={c.id} card={c} className="w-full" />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
84
src/components/aime-card.tsx
Normal file
84
src/components/aime-card.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { DB } from '@/types/db';
|
||||
import { useState } from 'react';
|
||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
||||
import { Button, Tooltip } from '@nextui-org/react';
|
||||
import { useUser } from '@/helpers/use-user';
|
||||
import { TbHammer, TbHammerOff, TbLock, TbLockOpen } from 'react-icons/tb';
|
||||
import { hasPermission } from '@/helpers/permissions';
|
||||
import { UserPermissions } from '@/types/permissions';
|
||||
import { banUnbanCard, lockUnlockCard } from '@/actions/card';
|
||||
|
||||
type AimeCardProps = {
|
||||
card: DB['aime_card'],
|
||||
className?: string,
|
||||
};
|
||||
|
||||
export const AimeCard = ({ className, card }: AimeCardProps) => {
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
const user = useUser();
|
||||
const canBan = hasPermission(user?.permissions, UserPermissions.USERMOD);
|
||||
const [locked, setLocked] = useState(!!card.is_locked ?? false);
|
||||
const [banned, setBanned] = useState(!!card.is_banned ?? true);
|
||||
const formatOptions = {
|
||||
year: '2-digit',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
} as const;
|
||||
|
||||
return (<div
|
||||
className={`${className ?? ''} relative flex flex-col max-w-md aspect-[3.37/2.125] rounded-2xl shadow bg-gradient-to-tr from-pink-700 to-rose-600 text-white p-4 gap-4 transition-all ${locked || banned ? 'brightness-50' : ''} ${banned ? 'grayscale' : ''}`}>
|
||||
<div className="flex gap-1">
|
||||
<div className="mr-auto mt-0.5 text-sm sm:text-medium">
|
||||
Registered {card.created_date?.toLocaleTimeString(undefined, formatOptions)}
|
||||
</div>
|
||||
|
||||
{canBan && <Tooltip content={banned ? 'Unban card' : 'Ban card'} size="sm">
|
||||
<Button isIconOnly variant="light" onPress={() => {
|
||||
setBanned(!banned);
|
||||
banUnbanCard({ cardId: card.id, userId: card.user, isBan: !banned })
|
||||
}}>
|
||||
{banned ? <TbHammerOff className="w-7 h-7 text-white" /> : <TbHammer className="w-7 h-7 text-white" />}
|
||||
</Button>
|
||||
</Tooltip>}
|
||||
|
||||
{!canBan && banned && <Tooltip content="This card is banned" size="sm">
|
||||
<Button isIconOnly variant="light" disabled>
|
||||
<TbHammer className="w-7 h-7 text-white" />
|
||||
</Button>
|
||||
</Tooltip>}
|
||||
|
||||
<Tooltip content={locked ? 'Unlock card' : 'Lock card'} size="sm">
|
||||
<Button isIconOnly variant="light" onPress={() => {
|
||||
setLocked(!locked);
|
||||
lockUnlockCard({ cardId: card.id, userId: card.user, isLock: !locked });
|
||||
}}>
|
||||
{locked ? <TbLock className="w-7 h-7 text-white" /> : <TbLockOpen className="w-7 h-7 text-white" />}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className={`[font-feature-settings:"fwid"] text-xs sm:text-medium font-semibold flex my-auto pb-4 sm:pb-0 gap-1 sm:gap-0 flex-wrap`}>
|
||||
{showCode ? <>
|
||||
{card.access_code?.match(/.{4}/g)?.join('-')}
|
||||
<EyeSlashIcon className="h-6 cursor-pointer ml-auto" onClick={() => setShowCode(false)} />
|
||||
</> :
|
||||
<>
|
||||
{('****-'.repeat(4) + card.access_code?.slice(-4))}
|
||||
<EyeIcon className="h-6 cursor-pointer ml-auto" onClick={() => setShowCode(true)} />
|
||||
</>}
|
||||
</div>
|
||||
|
||||
<div className="text-sm sm:text-medium">
|
||||
Last Used {card.last_login_date?.toLocaleTimeString(undefined, formatOptions)}
|
||||
</div>
|
||||
|
||||
{(locked || banned) && <div className="absolute flex items-center justify-center w-full left-0 top-[60%] backdrop-blur h-12 sm:h-16 bg-gray-600/50 font-bold sm:text-2xl">
|
||||
{locked ? 'Locked' : 'Banned'}
|
||||
</div>}
|
||||
</div>);
|
||||
};
|
22
src/helpers/permissions.ts
Normal file
22
src/helpers/permissions.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ArcadePermissions, UserPermissions } from '@/types/permissions';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
|
||||
|
||||
export const hasPermission = <T extends UserPermissions | ArcadePermissions>(userPermission: number | null | undefined, ...requestedPermission: T[]) => {
|
||||
if (!userPermission)
|
||||
return false;
|
||||
|
||||
if (userPermission & (1 << UserPermissions.OWNER))
|
||||
return true;
|
||||
|
||||
const permissionMask = requestedPermission
|
||||
.reduce((mask, permission) => mask | (1 << permission), 0);
|
||||
|
||||
return (userPermission & permissionMask) === permissionMask;
|
||||
}
|
||||
|
||||
export const requirePermission = <T extends UserPermissions | ArcadePermissions>(userPermission: number | null | undefined, ...requestedPermission: T[]) => {
|
||||
if (!hasPermission(userPermission, ...requestedPermission))
|
||||
redirect('/unauthorized');
|
||||
}
|
9
src/middleware.ts
Normal file
9
src/middleware.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const headers = new Headers(request.headers);
|
||||
headers.set('x-path', request.nextUrl.basePath + request.nextUrl.pathname);
|
||||
return NextResponse.next({
|
||||
request: { headers }
|
||||
});
|
||||
}
|
17
src/types/permissions.ts
Normal file
17
src/types/permissions.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export const enum UserPermissions {
|
||||
USER = 0, // regular user
|
||||
USERMOD = 1, // can moderate other users
|
||||
ACMOD = 2, // can add arcades and cabs
|
||||
SYSADMIN = 3, // can change settings
|
||||
|
||||
OWNER = 7 // can do anything
|
||||
}
|
||||
|
||||
export const enum ArcadePermissions {
|
||||
VIEW = 0, // view info and cabs
|
||||
BOOKKEEP = 1, // view bookkeeping info
|
||||
EDITOR = 2, // can edit name, settings
|
||||
REGISTRAR = 3, // can add cabs
|
||||
|
||||
OWNER = 7 // can do anything
|
||||
}
|
Loading…
Reference in New Issue
Block a user