Skip to content

Commit

Permalink
wip: added perms check to player modal
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed Dec 27, 2023
1 parent 80bd16e commit caf2050
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 23 deletions.
4 changes: 3 additions & 1 deletion docs/dev_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ setTimeout(() => {
- [x][3d] playerlist
- [ ][2d] implement new player modal
- [x] legacy pages should open the new modal
- [ ] write tsx + handling of data
- [x] write tsx + handling of data
- [ ] all actions
- [ ] clean legacy modal and playerlist code
- [ ] make sure it is responsive
Expand Down Expand Up @@ -307,6 +307,8 @@ Master Actions:

- [ ] Anonymous admin actions (issue #893)
- settings with select box for which options to choose (bans, warns, dms, kicks, restarts, announcements, everything)
- [ ] Change the kick all to require the `control.server` permission (issue #379)

- [ ] maybe use [this lib](https://www.npmjs.com/package/ntp-time-sync) to check for clock skew so i can remove the complexity of dealing with possible desync between core and ui on player modal, scheduler, etc;

- [ ] write some automated tests for the auth logic and middlewares
Expand Down
38 changes: 38 additions & 0 deletions panel/src/hooks/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ const csrfTokenAtom = atom((get) => {
const authData = get(authDataAtom);
return authData ? authData.csrfToken : undefined;
});
const adminPermissionsAtom = atom((get) => {
const authData = get(authDataAtom);
if(!authData) return undefined;
return {
permissions: authData.permissions,
isMaster: authData.isMaster,
};
});


/**
Expand All @@ -27,6 +35,36 @@ export const useCsrfToken = () => {
return useAtomValue(csrfTokenAtom);
};

//Admin permissions hook, only re-renders on perms change or login/logout
//Perms logic from core/components/WebServer/authLogic.ts
export const useAdminPerms = () => {
const permsData = useAtomValue(adminPermissionsAtom);

const hasPerm = (perm: string) => {
if(!permsData) return false;
try {
if (perm === 'master') {
return permsData.isMaster;
}
return (
permsData.isMaster
|| permsData.permissions.includes('all_permissions')
|| permsData.permissions.includes(perm)
);
} catch (error) {
console.error(`Error validating permission '${perm}' denied.`);
return false;
}
}

return {
hasPerm,
isMaster: permsData ? permsData.isMaster : false,
//NOTE: this one really shouldn't be used
// permissions: permsData ? permsData.permissions : [],
};
};

//Wipes auth data from the atom, this is triggered when an api pr page call returns a logout notice
//Since this is triggered by a logout notice, we don't need to bother doing a POST /auth/logout
export const useExpireAuthData = () => {
Expand Down
9 changes: 9 additions & 0 deletions panel/src/layout/playerModal/BanTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useAdminPerms } from "@/hooks/auth";
import { PlayerModalRefType } from "@/hooks/playerModal";
import { Loader2Icon } from "lucide-react";
import { useRef, useState } from "react";
import { PlayerModalMidMessage } from "./PlayerModal";


export default function BanTab({ playerRef }: { playerRef: Exclude<PlayerModalRefType, undefined> }) {
Expand All @@ -13,6 +15,13 @@ export default function BanTab({ playerRef }: { playerRef: Exclude<PlayerModalRe
const [customMultiplier, setCustomMultipler] = useState('2');
const [customUnits, setCustomUnits] = useState('days');
const [isSaving, setIsSaving] = useState(false);
const { hasPerm } = useAdminPerms();
if(!hasPerm('players.ban')) {
return <PlayerModalMidMessage>
You don't have permission to ban players.
</PlayerModalMidMessage>;
}


const handleSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault();
Expand Down
31 changes: 24 additions & 7 deletions panel/src/layout/playerModal/HistoryTab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Button } from "@/components/ui/button";
import { useAdminPerms } from "@/hooks/auth";
import { cn } from "@/lib/utils";
import { PlayerHistoryItem, PlayerModalSuccess } from "@shared/playerApiTypes";
import { PlayerModalMidMessage } from "./PlayerModal";


type HistoryItemProps = {
Expand All @@ -13,8 +15,8 @@ type HistoryItemProps = {
function HistoryItem({ action, permsDisableWarn, permsDisableBan, serverTime }: HistoryItemProps) {
const isRevokeDisabled = (
!!action.revokedBy ||
(action.type == 'warn' && permsDisableWarn) ||
(action.type == 'ban' && permsDisableBan)
(action.type === 'warn' && permsDisableWarn) ||
(action.type === 'ban' && permsDisableBan)
);
const actionDate = (new Date(action.ts * 1000)).toLocaleString();

Expand Down Expand Up @@ -57,15 +59,30 @@ function HistoryItem({ action, permsDisableWarn, permsDisableBan, serverTime }:
}


export default function HistoryTab({ actionHistory }: { actionHistory: PlayerHistoryItem[] }) {
type HistoryTabProps = {
actionHistory: PlayerHistoryItem[],
serverTime: number,
}

export default function HistoryTab({ actionHistory, serverTime }: HistoryTabProps) {
const { hasPerm } = useAdminPerms();
const hasWarnPerm = hasPerm('players.warn');
const hasBanPerm = hasPerm('players.ban');

if (!actionHistory.length) {
return <div className="flex items-center justify-center min-h-[16.5rem] p-1">
<span className="text-xl text-muted-foreground">No bans/warns found.</span>
</div>;
return <PlayerModalMidMessage>
No bans/warns found.
</PlayerModalMidMessage>;
} else {
return <div className="flex flex-col gap-1 p-1">
{actionHistory.map((action) => (
<HistoryItem key={action.id} action={action} permsDisableWarn={false} permsDisableBan={false} serverTime={0} />
<HistoryItem
key={action.id}
action={action}
permsDisableWarn={!hasWarnPerm}
permsDisableBan={!hasBanPerm}
serverTime={serverTime}
/>
))}
</div>;
}
Expand Down
5 changes: 4 additions & 1 deletion panel/src/layout/playerModal/InfoTab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useAdminPerms } from "@/hooks/auth";
import { cn, msToDuration, tsToLocaleDate } from "@/lib/utils";
import { PlayerModalPlayerData } from "@shared/playerApiTypes";
import { useRef, useState } from "react";
Expand Down Expand Up @@ -67,6 +68,8 @@ type InfoTabProps = {
}

export default function InfoTab({ player, setSelectedTab }: InfoTabProps) {
const { hasPerm } = useAdminPerms();

const sessionTimeText = player.sessionTime ? msToDuration(
player.sessionTime * 60_000,
{ units: ['h', 'm'] }
Expand Down Expand Up @@ -111,7 +114,7 @@ export default function InfoTab({ player, setSelectedTab }: InfoTabProps) {
size='inline'
style={{ minWidth: '8.25ch' }}
onClick={() => { }} //FIXME:
disabled={false} //FIXME:
disabled={!hasPerm('players.whitelist')}
>
{player.tsWhitelisted ? 'Remove' : 'Add WL'}
</Button>
Expand Down
49 changes: 35 additions & 14 deletions panel/src/layout/playerModal/PlayerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,27 @@ import GenericSpinner from "@/components/GenericSpinner";
import { cn } from "@/lib/utils";
import { useBackendApi } from "@/hooks/fetch";
import { PlayerModalResp, PlayerModalSuccess } from "@shared/playerApiTypes";
import { useAdminPerms } from "@/hooks/auth";


export function PlayerModalMidMessage({children}: {children: React.ReactNode}) {
return (
<div className="flex items-center justify-center min-h-[16.5rem] text-xl p1 text-muted-foreground">
{children}
</div>
)
}


function PlayerModalFooter() {
const { hasPerm } = useAdminPerms();

return (
<DialogFooter className="max-w-2xl gap-2 p-2 md:p-4 border-t grid grid-cols-2 sm:flex">
<Button
variant='outline'
size='sm'
disabled={false} //FIXME:
disabled={!hasPerm('manage.admins')}
onClick={() => { }} //FIXME:
className="pl-2 sm:mr-auto"
>
Expand All @@ -35,7 +47,7 @@ function PlayerModalFooter() {
<Button
variant='outline'
size='sm'
disabled={false} //FIXME:
disabled={!hasPerm('players.message')}
onClick={() => { }} //FIXME:
className="pl-2"
>
Expand All @@ -44,7 +56,7 @@ function PlayerModalFooter() {
<Button
variant='outline'
size='sm'
disabled={false} //FIXME:
disabled={!hasPerm('players.kick')}
onClick={() => { }} //FIXME:
className="pl-2"
>
Expand All @@ -58,7 +70,7 @@ function PlayerModalFooter() {
<Button
variant='outline'
size='sm'
disabled={false} //FIXME:
disabled={!hasPerm('players.warn')}
onClick={() => { }} //FIXME:
className="pl-2"
>
Expand Down Expand Up @@ -140,7 +152,7 @@ export default function PlayerModal() {
let pageTitle: JSX.Element;
if (modalData) {
pageTitle = <>
<span className="text-muted-foreground">[{modalData.player.netid || 'OFFLINE'}]</span> {modalData.player.displayName}
<span className="text-muted-foreground font-mono">[{modalData.player.netid || 'OFFLINE'}]</span> {modalData.player.displayName}
</>;
} else if (modalError) {
pageTitle = <span className="text-destructive-inline">Error!</span>;
Expand Down Expand Up @@ -179,25 +191,34 @@ export default function PlayerModal() {
{/* NOTE: consistent height: sm:h-[16.5rem] */}
<ScrollArea className="w-full max-h-[calc(100vh-3.125rem-4rem-5rem)] min-h-[16.5rem] px-4 py-2 md:py-0">
{!modalData ? (
<div className="flex items-center justify-center min-h-[16.5rem]">
<PlayerModalMidMessage>
{modalError ? (
<span className="text-xl text-destructive-inline">Error: {modalError}</span>
<span className="text-destructive-inline">Error: {modalError}</span>
) : (
<GenericSpinner msg="Loading..." />
)}
</div>
</PlayerModalMidMessage>
) : (
<>
{selectedTab === 'Info' && <InfoTab player={modalData.player} setSelectedTab={setSelectedTab} />}
{selectedTab === 'History' && <HistoryTab actionHistory={modalData.player.actionHistory} />}
{selectedTab === 'IDs' && <IdsTab player={modalData.player} />}
{selectedTab === 'Ban' && <BanTab playerRef={playerRef!} />}
{selectedTab === 'Info' && <InfoTab
player={modalData.player}
setSelectedTab={setSelectedTab}
/>}
{selectedTab === 'History' && <HistoryTab
actionHistory={modalData.player.actionHistory}
serverTime={modalData.meta.serverTime}
/>}
{selectedTab === 'IDs' && <IdsTab
player={modalData.player}
/>}
{selectedTab === 'Ban' && <BanTab
playerRef={playerRef!}
/>}
</>
)}
</ScrollArea>
</div>

<PlayerModalFooter /> {/* FIXME: */}
<PlayerModalFooter />
</DialogContent>
</Dialog>
);
Expand Down

0 comments on commit caf2050

Please sign in to comment.