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": "^18",
|
||||||
"react-day-picker": "^8.10.0",
|
"react-day-picker": "^8.10.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-icons": "^5.0.1",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.71.1",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
@ -9582,6 +9583,14 @@
|
|||||||
"react": "^18.2.0"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-day-picker": "^8.10.0",
|
"react-day-picker": "^8.10.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-icons": "^5.0.1",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.71.1",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
|
@ -6,15 +6,30 @@ import { auth, signIn, signOut } from '@/auth';
|
|||||||
import { AuthError } from 'next-auth';
|
import { AuthError } from 'next-auth';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
|
import { UserPermissions } from '@/types/permissions';
|
||||||
|
import { requirePermission } from '@/helpers/permissions';
|
||||||
|
import { UserPayload } from '@/types/user';
|
||||||
|
|
||||||
export const getUser = async () => {
|
export const getUser = async () => {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
return session?.user;
|
return session?.user;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requireUser = async () => {
|
type RequireUserOptions = {
|
||||||
return await getUser() ??
|
permission?: UserPermissions[] | UserPermissions
|
||||||
redirect(`/auth/login?error=1&callbackUrl=${encodeURIComponent(new URL(headers().get('referer') ?? 'http://a/').pathname)}`);
|
};
|
||||||
|
|
||||||
|
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 = {
|
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() {
|
import { getCards } from '@/actions/card';
|
||||||
return (<div>settings</div>);
|
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