chore: call ban/unban user by authClient.admin

This commit is contained in:
javayhu 2025-05-11 10:44:48 +08:00
parent d889cdf2b7
commit 78681df65f
4 changed files with 42 additions and 242 deletions

View File

@ -1,47 +0,0 @@
'use server';
import db from '@/db';
import { user } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Ban user schema for validation
const banUserSchema = z.object({
userId: z.string().min(1, { message: 'User ID is required' }),
reason: z.string().optional(),
expiresAt: z.date().nullable(),
});
/**
* Ban a user
*/
export const banUserAction = actionClient
.schema(banUserSchema)
.action(async ({ parsedInput }) => {
try {
const { userId, reason, expiresAt } = parsedInput;
await db
.update(user)
.set({
banned: true,
banReason: reason,
banExpires: expiresAt,
})
.where(eq(user.id, userId));
return {
success: true,
};
} catch (error) {
console.error('Ban user error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to ban user',
};
}
});

View File

@ -1,45 +0,0 @@
'use server';
import db from '@/db';
import { user } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';
// Create a safe action client
const actionClient = createSafeActionClient();
// Unban user schema for validation
const unbanUserSchema = z.object({
userId: z.string().min(1, { message: 'User ID is required' }),
});
/**
* Unban a user
*/
export const unbanUserAction = actionClient
.schema(unbanUserSchema)
.action(async ({ parsedInput }) => {
try {
const { userId } = parsedInput;
await db
.update(user)
.set({
banned: false,
banReason: null,
banExpires: null,
})
.where(eq(user.id, userId));
return {
success: true,
};
} catch (error) {
console.error('Unban user error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to unban user',
};
}
});

View File

@ -16,6 +16,7 @@ import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { useIsMobile } from '@/hooks/use-mobile';
import { authClient } from '@/lib/auth-client';
import { formatDate } from '@/lib/formatter';
import {
Loader2Icon,
@ -25,46 +26,19 @@ import {
UserRoundXIcon,
} from 'lucide-react';
import { useTranslations } from 'next-intl';
import type { SafeActionResult } from 'next-safe-action';
import { useState } from 'react';
import { toast } from 'sonner';
export interface User {
id: string;
name: string;
email: string;
emailVerified: boolean;
image: string | null;
role: string | null;
createdAt: Date;
updatedAt: Date;
customerId: string | null;
banned: boolean | null;
banReason: string | null;
banExpires: Date | null;
}
import type { User } from './users-table';
interface UserDetailViewerProps {
user: User;
onBan: (
userId: string,
reason: string,
expiresAt: Date | null
) => Promise<SafeActionResult<string, any, any, any, any>>;
onUnban: (
userId: string
) => Promise<SafeActionResult<string, any, any, any, any>>;
}
export function UserDetailViewer({
user,
onBan,
onUnban,
}: UserDetailViewerProps) {
export function UserDetailViewer({ user }: UserDetailViewerProps) {
const t = useTranslations('Dashboard.admin.users');
const isMobile = useIsMobile();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | undefined>('');
const [error, setError] = useState<string | undefined>();
const [banReason, setBanReason] = useState('');
const [banExpiresAt, setBanExpiresAt] = useState<string>('');
@ -74,70 +48,58 @@ export function UserDetailViewer({
return;
}
if (!user.id) {
setError('User ID is required');
return;
}
setIsLoading(true);
setError('');
try {
const expiresAt = banExpiresAt ? new Date(banExpiresAt) : null;
const result = await onBan(user.id, banReason, expiresAt);
await authClient.admin.banUser({
userId: user.id,
banReason,
banExpiresIn: expiresAt
? Math.floor((expiresAt.getTime() - Date.now()) / 1000)
: undefined,
});
if (
result?.data &&
typeof result.data === 'object' &&
'success' in result.data &&
result.data.success
) {
toast.success(t('ban.success'));
// Reset form
setBanReason('');
setBanExpiresAt('');
} else {
const errorMessage =
result?.data &&
typeof result.data === 'object' &&
'error' in result.data
? String(result.data.error)
: t('ban.error');
toast.error(errorMessage);
setError(errorMessage);
}
} catch (error) {
console.error('ban user error:', error);
setError(t('ban.error'));
toast.error(t('ban.error'));
toast.success(t('ban.success'));
// Reset form
setBanReason('');
setBanExpiresAt('');
} catch (err) {
const error = err as Error;
console.error('Failed to ban user:', error);
setError(error.message || t('ban.error'));
toast.error(error.message || t('ban.error'));
} finally {
setIsLoading(false);
}
};
const handleUnban = async () => {
if (!user.id) {
setError('User ID is required');
return;
}
setIsLoading(true);
setError('');
try {
const result = await onUnban(user.id);
await authClient.admin.unbanUser({
userId: user.id,
});
if (
result?.data &&
typeof result.data === 'object' &&
'success' in result.data &&
result.data.success
) {
toast.success(t('unban.success'));
} else {
const errorMessage =
result?.data &&
typeof result.data === 'object' &&
'error' in result.data
? String(result.data.error)
: t('unban.error');
toast.error(errorMessage);
setError(errorMessage);
}
} catch (error) {
console.error('unban user error:', error);
setError(t('unban.error'));
toast.error(t('unban.error'));
toast.success(t('unban.success'));
} catch (err) {
const error = err as Error;
console.error('Failed to unban user:', error);
setError(error.message || t('unban.error'));
toast.error(error.message || t('unban.error'));
} finally {
setIsLoading(false);
}
@ -246,7 +208,7 @@ export function UserDetailViewer({
variant="destructive"
onClick={handleUnban}
disabled={isLoading}
className="mt-4"
className="mt-4 cursor-pointer"
>
{isLoading && (
<Loader2Icon className="mr-2 size-4 animate-spin" />
@ -285,7 +247,7 @@ export function UserDetailViewer({
type="submit"
variant="destructive"
disabled={isLoading || !banReason}
className="mt-4"
className="mt-4 cursor-pointer"
>
{isLoading && (
<Loader2Icon className="mr-2 size-4 animate-spin" />

View File

@ -1,14 +1,11 @@
'use client';
import { banUserAction } from '@/actions/ban-user';
import { unbanUserAction } from '@/actions/unban-user';
import { UserDetailViewer } from '@/components/admin/user-detail-viewer';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
@ -50,7 +47,6 @@ import {
ChevronsRightIcon,
MailCheckIcon,
MailQuestionIcon,
MoreVerticalIcon,
UserRoundCheckIcon,
UserRoundXIcon,
} from 'lucide-react';
@ -111,41 +107,7 @@ const columns: ColumnDef<User>[] = [
),
cell: ({ row }) => {
const user = row.original;
return (
<UserDetailViewer
user={user}
onBan={async (userId, reason, expiresAt) => {
const result = await banUserAction({ userId, reason, expiresAt });
if (!result) {
throw new Error('Failed to ban user');
}
if (result.validationErrors) {
throw new Error(
Object.values(result.validationErrors).join(', ')
);
}
if (result.serverError) {
throw new Error(result.serverError);
}
return result;
}}
onUnban={async (userId) => {
const result = await unbanUserAction({ userId });
if (!result) {
throw new Error('Failed to unban user');
}
if (result.validationErrors) {
throw new Error(
Object.values(result.validationErrors).join(', ')
);
}
if (result.serverError) {
throw new Error(result.serverError);
}
return result;
}}
/>
);
return <UserDetailViewer user={user} />;
},
},
{
@ -284,38 +246,6 @@ const columns: ColumnDef<User>[] = [
);
},
},
{
id: 'actions',
header: 'Actions',
cell: ({ row }) => {
const user = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="cursor-pointer data-[state=open]:bg-muted text-muted-foreground flex size-8"
size="icon"
>
<MoreVerticalIcon />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
{user.banned ? (
<DropdownMenuItem className="cursor-pointer">
Unban User
</DropdownMenuItem>
) : (
<DropdownMenuItem className="cursor-pointer">
Ban User
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
interface UsersTableProps {