aau-auth-kit-ui
Version:
Plug & play shadcn/ui components for aau-auth-kit with Next.js integration
1,393 lines (1,381 loc) • 280 kB
JavaScript
"use client";
import {
AuthUIContext,
AuthUIProvider,
Button,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
authViewPaths,
cn,
getAuthViewByPath,
getLocalizedError,
getSearchParam,
isValidEmail
} from "./chunk-HRQRKYY3.js";
// src/components/admin/admin-page.tsx
import { Search, X } from "lucide-react";
import { useCallback as useCallback2, useEffect as useEffect3, useRef, useState as useState3 } from "react";
// src/hooks/use-admin-users.tsx
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
var ERROR_MESSAGES = {
fetchUsers: "Failed to load users. Please try again.",
banUser: "Unable to ban user. The operation failed.",
unbanUser: "Unable to unban user. The operation failed.",
removeUser: "Unable to remove user. The operation failed.",
setRole: "Unable to change user role. The operation failed.",
revokeSessions: "Unable to revoke user sessions. The operation failed.",
invalidResponse: "The server returned an invalid response.",
networkError: "Network connection error. Please check your internet connection.",
serverError: "The server encountered an error. Please try again later.",
unauthorized: "You don't have permission to perform this action.",
rateLimited: "Too many requests. Please try again in a moment.",
unknown: "An unexpected error occurred. Please try again."
};
function useAdminUsers(initialFilters = {}) {
const { authClient, toast } = useContext(AuthUIContext);
const [users, setUsers] = useState([]);
const [totalUsers, setTotalUsers] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [isRefetching, setIsRefetching] = useState(false);
const [isActionPending, setIsActionPending] = useState(false);
const [error, setError] = useState(null);
const [initialFetchDone, setInitialFetchDone] = useState(false);
const [filters, setFilters] = useState({
searchQuery: "",
filterRole: null,
sortBy: "createdAt",
sortDirection: "desc",
currentPage: 1,
pageSize: 10,
...initialFilters
});
const totalPages = Math.ceil(totalUsers / filters.pageSize);
const hasError = error !== null;
const queryParams = useMemo(() => {
const params = {
limit: filters.pageSize,
offset: (filters.currentPage - 1) * filters.pageSize,
sortBy: filters.sortBy,
sortDirection: filters.sortDirection,
search: ""
};
if (filters.searchQuery) {
params.search = filters.searchQuery;
}
return params;
}, [filters]);
const e = useCallback(
(errorMessage, errorCode, action, userId) => {
const errorState = {
code: errorCode,
message: errorMessage,
action,
userId,
timestamp: Date.now()
};
setError(errorState);
toast({
variant: "error",
message: errorMessage
});
return errorState;
},
[toast]
);
const handleError = useCallback(
(error2, action, userId) => {
let errorCode = error2?.code;
let errorMessage = error2.message;
if (errorCode && errorMessage)
return e(errorMessage, errorCode, action, userId);
if (error2.code) {
errorCode = error2.code;
} else if (error2.status) {
errorCode = String(error2.status);
}
switch (errorCode) {
case "NETWORK_ERROR":
errorMessage = ERROR_MESSAGES.networkError;
break;
case "RATE_LIMITED":
case "429":
errorMessage = ERROR_MESSAGES.rateLimited;
break;
case "401":
case "403":
errorMessage = ERROR_MESSAGES.unauthorized;
break;
case "500":
case "502":
case "503":
case "504":
errorMessage = ERROR_MESSAGES.serverError;
break;
default:
if (action === "fetchUsers") {
errorMessage = ERROR_MESSAGES.fetchUsers;
} else if (action === "banUser") {
errorMessage = ERROR_MESSAGES.banUser;
} else if (action === "unbanUser") {
errorMessage = ERROR_MESSAGES.unbanUser;
} else if (action === "removeUser") {
errorMessage = ERROR_MESSAGES.removeUser;
} else if (action === "setRole") {
errorMessage = ERROR_MESSAGES.setRole;
} else if (action === "revokeSessions") {
errorMessage = ERROR_MESSAGES.revokeSessions;
}
}
if (error2.message) {
errorMessage = error2.message;
}
return e(errorMessage, errorCode, action, userId);
},
[e]
);
const clearError = useCallback(() => {
setError(null);
}, []);
const fetchUsers = useCallback(
async (options = {}) => {
const { retryCount = 0, silentRefetch = false } = options;
if (!initialFetchDone || !silentRefetch) {
setIsLoading(true);
} else {
setIsRefetching(true);
}
clearError();
try {
const response = await authClient.admin.listUsers({
query: queryParams
});
if (!response || !response.data || !Array.isArray(response.data.users)) {
throw new Error(ERROR_MESSAGES.invalidResponse);
}
const formattedUsers = response.data.users.map((user) => ({
id: user.id,
name: user.name || "N/A",
email: user.email || "No email",
role: user.role || "user",
verified: !!user.emailVerified,
status: user.banned ? "banned" : "active",
createdAt: user.createdAt ? new Date(user.createdAt).toLocaleDateString() : "Unknown"
}));
setUsers(formattedUsers);
setTotalUsers(formattedUsers.length);
setInitialFetchDone(true);
} finally {
setIsLoading(false);
setIsRefetching(false);
}
},
[authClient, queryParams, handleError, clearError, initialFetchDone]
);
const updateFilters = useCallback(
(newFilters, silentRefetch = false) => {
setFilters((prev) => ({
...prev,
...newFilters,
currentPage: "currentPage" in newFilters ? newFilters.currentPage : 1
}));
if (silentRefetch) {
return { silentRefetch: true };
}
return { silentRefetch: false };
},
[]
);
const handleUserAction = useCallback(
async (action, userId, data) => {
if (isActionPending) return { success: false };
setIsActionPending(true);
clearError();
try {
if (!userId) {
throw new Error("Invalid user ID");
}
switch (action) {
case "ban":
await authClient.admin.banUser(
{ userId },
{
onSuccess() {
toast({
variant: "success",
message: "User banned successfully"
});
},
onError(ctx) {
console.log({ ctx });
handleError(ctx?.error, `${action}User`, userId);
}
}
);
break;
case "unban":
await authClient.admin.unbanUser(
{
userId
},
{
onSuccess() {
toast({
variant: "success",
message: "User unbanned successfully"
});
},
onError(ctx) {
console.log({ ctx });
handleError(ctx?.error, `${action}User`, userId);
}
}
);
break;
case "remove":
await authClient.admin.removeUser(
{ userId },
{
onSuccess() {
toast({
variant: "success",
message: "User removed successfully"
});
},
onError(ctx) {
console.log({ ctx });
handleError(ctx?.error, `${action}User`, userId);
}
}
);
break;
case "setRole":
await authClient.admin.setRole(
{ userId, role: data.role },
{
onSuccess() {
toast({
variant: "success",
message: "User role updated successfully"
});
},
onError(ctx) {
console.log({ ctx });
handleError(ctx?.error, action, userId);
}
}
);
break;
case "revokeSessions":
await authClient.admin.revokeUserSessions(
{ userId },
{
onSuccess() {
toast({
variant: "success",
message: "User sessions revoked successfully"
});
},
onError(ctx) {
console.log({ ctx });
handleError(ctx?.error, action, userId);
}
}
);
break;
default:
throw new Error(`Unsupported action: ${action}`);
}
fetchUsers({ silentRefetch: true });
return { success: true };
} catch (error2) {
return { success: false };
} finally {
setIsActionPending(false);
}
},
[authClient, fetchUsers, isActionPending, toast, handleError, clearError]
);
const retry = useCallback(() => {
if (!error) return;
const { action, userId } = error;
clearError();
if (action === "fetchUsers") {
fetchUsers({ retryCount: 1 });
} else if (action?.endsWith("User") && userId) {
const originalAction = action.replace("User", "");
handleUserAction(originalAction, userId);
}
}, [error, clearError, fetchUsers, handleUserAction]);
useEffect(() => {
fetchUsers({ silentRefetch: initialFetchDone });
}, [fetchUsers, initialFetchDone]);
return {
// Data
users,
totalUsers,
totalPages,
// State
isLoading,
isRefetching,
// New state for tracking silent refetches
isActionPending,
error,
hasError,
// Filters
filters,
updateFilters,
// Actions
fetchUsers,
handleUserAction,
retry,
clearError
};
}
// src/components/ui/card.tsx
import { jsx } from "react/jsx-runtime";
function Card({ className, ...props }) {
return /* @__PURE__ */ jsx(
"div",
{
"data-slot": "card",
className: cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
),
...props
}
);
}
function CardHeader({ className, ...props }) {
return /* @__PURE__ */ jsx(
"div",
{
"data-slot": "card-header",
className: cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
),
...props
}
);
}
function CardTitle({ className, ...props }) {
return /* @__PURE__ */ jsx(
"div",
{
"data-slot": "card-title",
className: cn("leading-none font-semibold", className),
...props
}
);
}
function CardDescription({ className, ...props }) {
return /* @__PURE__ */ jsx(
"div",
{
"data-slot": "card-description",
className: cn("text-muted-foreground text-sm", className),
...props
}
);
}
function CardContent({ className, ...props }) {
return /* @__PURE__ */ jsx(
"div",
{
"data-slot": "card-content",
className: cn("px-6", className),
...props
}
);
}
function CardFooter({ className, ...props }) {
return /* @__PURE__ */ jsx(
"div",
{
"data-slot": "card-footer",
className: cn("flex items-center px-6 [.border-t]:pt-6", className),
...props
}
);
}
// src/components/admin/create-user-dialog.tsx
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2 } from "lucide-react";
import { useContext as useContext2, useEffect as useEffect2 } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
// src/lib/auth-localization.ts
var authLocalization = {
/** @default "Account" */
account: "Account",
/** @default "Accounts" */
accounts: "Accounts",
/** @default "Manage your currently signed in accounts." */
accountsDescription: "Switch between your currently signed in accounts.",
/** @default "Sign in to an additional account." */
accountsInstructions: "Sign in to an additional account.",
/** @default "Add Account" */
addAccount: "Add Account",
/** @default "Add Passkey" */
addPasskey: "Add Passkey",
/** @default "Already have an account?" */
alreadyHaveAnAccount: "Already have an account?",
/** @default "Avatar" */
avatar: "Avatar",
/** @default "Click on the avatar to upload a custom one from your files." */
avatarDescription: "Click on the avatar to upload a custom one from your files.",
/** @default "An avatar is optional but strongly recommended." */
avatarInstructions: "An avatar is optional but strongly recommended.",
/** @default "Backup code is required" */
backupCodeRequired: "Backup code is required",
/** @default "Backup Codes" */
backupCodes: "Backup Codes",
/** @default "Save these backup codes in a secure place. You can use them to access your account if you lose your two-factor authentication method." */
backupCodesDescription: "Save these backup codes in a secure place. You can use them to access your account if you lose your two-factor authentication method.",
/** @default "Enter one of your backup codes. Once used, each code can only be used once and will be invalidated after use." */
backupCodePlaceholder: "Backup Code",
/** @default "Backup Code" */
backupCode: "Backup Code",
/** @default "Recover account" */
backupCodeAction: "Recover account",
/** @default "Cancel" */
cancel: "Cancel",
/** @default "Change Password" */
changePassword: "Change Password",
/** @default "Enter your current password and a new password." */
changePasswordDescription: "Enter your current password and a new password.",
/** @default "Please use 8 characters at minimum." */
changePasswordInstructions: "Please use 8 characters at minimum.",
/** @default "Your password has been changed." */
changePasswordSuccess: "Your password has been changed.",
/** @default "Confirm Password" */
confirmPassword: "Confirm Password",
/** @default "Confirm Password" */
confirmPasswordPlaceholder: "Confirm Password",
/** @default "Confirm password is required" */
confirmPasswordRequired: "Confirm password is required",
/** @default "Continue with Authenticator" */
continueWithAuthenticator: "Continue with Authenticator",
/** @default "Copied to clipboard" */
copiedToClipboard: "Copied to clipboard",
/** @default "Copy all codes" */
copyAllCodes: "Copy all codes",
/** @default "Continue" */
continue: "Continue",
/** @default "Current Password" */
currentPassword: "Current Password",
/** @default "Current Password" */
currentPasswordPlaceholder: "Current Password",
/** @default "Current Session" */
currentSession: "Current Session",
/** @default "Delete" */
delete: "Delete",
/** @default "Delete Account" */
deleteAccount: "Delete Account",
/** @default "Permanently remove your account and all of its contents. This action is not reversible, so please continue with caution." */
deleteAccountDescription: "Permanently remove your account and all of its contents. This action is not reversible, so please continue with caution.",
/** @default "Please confirm the deletion of your account. This action is not reversible, so please continue with caution." */
deleteAccountInstructions: "Please confirm the deletion of your account. This action is not reversible, so please continue with caution.",
/** @default "Please check your email to verify the deletion of your account." */
deleteAccountVerify: "Please check your email to verify the deletion of your account.",
/** @default "Your account has been deleted." */
deleteAccountSuccess: "Your account has been deleted.",
/** @default "You must be recently logged in to delete your account." */
deleteAccountNotFresh: "You must be recently logged in to delete your account.",
/** @default "Disable" */
disable: "Disable",
/** @default "Choose a provider to login to your account" */
disabledCredentialsDescription: "Choose a provider to login to your account",
/** @default "Don't have an account?" */
dontHaveAnAccount: "Don't have an account?",
/** @default "Email" */
email: "Email",
/** @default "Enter the email address you want to use to log in." */
emailDescription: "Enter the email address you want to use to log in.",
/** @default "Please enter a valid email address." */
emailInstructions: "Please enter a valid email address.",
/** @default "Email address is invalid" */
emailInvalid: "Email address is invalid",
/** @default "Phone" */
phone: "Phone",
/** @default "Phone Number" */
phoneNumber: "Phone Number",
/** @default "Enter your phone number" */
phonePlaceholder: "+1 (555) 123-4567",
/** @default "Please enter a valid phone number" */
phoneInvalid: "Please enter a valid phone number",
/** @default "We'll send you a verification code" */
phoneDescription: "We'll send you a verification code",
/** @default "Verification code sent successfully" */
otpSent: "Verification code sent successfully",
/** @default "Phone number verified successfully" */
phoneVerified: "Phone number verified successfully",
/** @default "Verify" */
verifyPhoneNumber: "Verify",
/** @default "Enter the verification code sent to" */
enterVerificationCode: "Enter the 6-digit code sent to",
/** @default "Verification Code" */
verificationCode: "Verification Code",
/** @default "Resend Code" */
resendCode: "Resend Code",
/** @default "One Time Password" */
oneTimePassword: "One Time Password",
/** @default "Email is the same" */
emailIsTheSame: "Email is the same",
/** @default "m@example.com" */
emailPlaceholder: "m@example.com",
/** @default "Email address is required" */
emailRequired: "Email address is required",
/** @default "Please check your email to verify the change." */
emailVerifyChange: "Please check your email to verify the change.",
/** @default "Please check your email for the verification link." */
emailVerification: "Please check your email for the verification link.",
/** @default "Enable" */
enable: "Enable",
/** @default "Error" */
error: "Error",
/** @default "is invalid" */
isInvalid: "is invalid",
/** @default "is required" */
isRequired: "is required",
/** @default "is the same" */
isTheSame: "is the same",
/** @default "Forgot authenticator?" */
forgotAuthenticator: "Forgot authenticator?",
/** @default "Forgot Password" */
forgotPassword: "Forgot Password",
/** @default "Send reset link" */
forgotPasswordAction: "Send reset link",
/** @default "Enter your email to reset your password" */
forgotPasswordDescription: "Enter your email to reset your password",
/** @default "Check your email for the password reset link." */
forgotPasswordEmail: "Check your email for the password reset link.",
/** @default "Forgot your password?" */
forgotPasswordLink: "Forgot your password?",
/** @default "Invalid two factor cookie" */
invalidTwoFactorCookie: "Invalid two factor cookie",
/** @default "Link" */
link: "Link",
/** @default "Magic Link" */
magicLink: "Magic Link",
/** @default "Send magic link" */
magicLinkAction: "Send magic link",
/** @default "Enter your email to receive a magic link" */
magicLinkDescription: "Enter your email to receive a magic link",
/** @default "Check your email for the magic link" */
magicLinkEmail: "Check your email for the magic link",
/** @default "Email Code" */
emailOTP: "Email Code",
/** @default "Send code" */
emailOTPSendAction: "Send code",
/** @default "Verify code" */
emailOTPVerifyAction: "Verify code",
/** @default "Enter your email to receive a code" */
emailOTPDescription: "Enter your email to receive a code",
/** @default "Please check your email for the verification code." */
emailOTPVerificationSent: "Please check your email for the verification code.",
/** @default "Name" */
name: "Name",
/** @default "Please enter your full name, or a display name." */
nameDescription: "Please enter your full name, or a display name.",
/** @default "Please use 32 characters at maximum." */
nameInstructions: "Please use 32 characters at maximum.",
/** @default "Name" */
namePlaceholder: "Name",
/** @default "New Password" */
newPassword: "New Password",
/** @default "New Password" */
newPasswordPlaceholder: "New Password",
/** @default "New password is required" */
newPasswordRequired: "New password is required",
/** @default "Or continue with" */
orContinueWith: "Or continue with",
/** @default "Passkey" */
passkey: "Passkey",
/** @default "Passkeys" */
passkeys: "Passkeys",
/** @default "Manage your passkeys for secure access." */
passkeysDescription: "Manage your passkeys for secure access.",
/** @default "Securely access your account without a password." */
passkeysInstructions: "Securely access your account without a password.",
/** @default "Password" */
password: "Password",
/** @default "Password" */
passwordPlaceholder: "Password",
/** @default "Password is required" */
passwordRequired: "Password is required",
/** @default "Passwords do not match" */
passwordsDoNotMatch: "Passwords do not match",
/** @default "Providers" */
providers: "Providers",
/** @default "Connect your account with a third-party service." */
providersDescription: "Connect your account with a third-party service.",
/** @default "Recover Account" */
recoverAccount: "Recover Account",
/** @default "Recover account" */
recoverAccountAction: "Recover account",
/** @default "Please enter a backup code to access your account" */
recoverAccountDescription: "Please enter a backup code to access your account",
/** @default "Remember me" */
rememberMe: "Remember me",
/** @default "Resend verification email" */
resendVerificationEmail: "Resend Verification Email",
/** @default "Reset Password" */
resetPassword: "Reset Password",
/** @default "Save new password" */
resetPasswordAction: "Save new password",
/** @default "Enter your new password below" */
resetPasswordDescription: "Enter your new password below",
/** @default "Invalid reset password link" */
resetPasswordInvalidToken: "Invalid reset password link",
/** @default "Password reset successfully" */
resetPasswordSuccess: "Password reset successfully",
/** @default "Request failed" */
requestFailed: "Request failed",
/** @default "Revoke" */
revoke: "Revoke",
/** @default "Sign In" */
signIn: "Sign In",
/** @default "Sign In" */
signInPhone: "Sign In",
/** @default "Login" */
signInAction: "Login",
/** @default "Enter your email below to login to your account" */
signInDescription: "Enter your email below to login to your account",
/** @default "Enter your username or email below to login to your account" */
signInUsernameDescription: "Enter your username or email to login to your account",
/** @default "Sign in with" */
signInWith: "Sign in with",
/** @default "Sign Out" */
signOut: "Sign Out",
/** @default "Sign Up" */
signUp: "Sign Up",
/** @default "Create an account" */
signUpAction: "Create an account",
/** @default "Enter your information to create an account" */
signUpDescription: "Enter your information to create an account",
/** @default "Check your email for the verification link." */
signUpEmail: "Check your email for the verification link.",
/** @default "Sessions" */
sessions: "Sessions",
/** @default "Manage your active sessions and revoke access." */
sessionsDescription: "Manage your active sessions and revoke access.",
/** @default "Set Password" */
setPassword: "Set Password",
/** @default "Click the button below to receive an email to set up a password for your account." */
setPasswordDescription: "Click the button below to receive an email to set up a password for your account.",
/** @default "Settings" */
settings: "Settings",
/** @default "Save" */
save: "Save",
/** @default "Security" */
security: "Security",
/** @default "Switch Account" */
switchAccount: "Switch Account",
/** @default "Trust this device" */
trustDevice: "Trust this device",
/** @default "Two-Factor" */
twoFactor: "Two-Factor",
/** @default "Verify code" */
twoFactorAction: "Verify code",
/** @default "Please enter your one-time password to continue" */
twoFactorDescription: "Please enter your one-time password to continue",
/** @default "Add an extra layer of security to your account." */
twoFactorCardDescription: "Add an extra layer of security to your account.",
/** @default "Please enter your password to disable 2FA." */
twoFactorDisableInstructions: "Please enter your password to disable 2FA.",
/** @default "Please enter your password to enable 2FA" */
twoFactorEnableInstructions: "Please enter your password to enable 2FA.",
/** @default "Two-factor authentication has been enabled" */
twoFactorEnabled: "Two-factor authentication has been enabled",
/** @default "Two-Factor Authentication has been disabled" */
twoFactorDisabled: "Two-Factor Authentication has been disabled",
/** @default "Two-Factor Authentication" */
twoFactorPrompt: "Two-Factor Authentication",
/** @default "Scan the QR Code with your Authenticator" */
twoFactorTotpLabel: "Scan the QR Code with your Authenticator",
/** @default "Send verification code" */
sendVerificationCode: "Send verification code",
/** @default "Unlink" */
unlink: "Unlink",
/** @default "Updated successfully" */
updatedSuccessfully: "updated successfully",
/** @default "Username" */
username: "Username",
/** @default "Enter the username you want to use to log in." */
usernameDescription: "Enter the username you want to use to log in.",
/** @default "Please use 32 characters at maximum." */
usernameInstructions: "Please use 32 characters at maximum.",
/** @default "Username" */
usernamePlaceholder: "Username",
/** @default "Username or email" */
signInUsernamePlaceholder: "Username or email",
/** @default "Verify Your Email" */
verifyYourEmail: "Verify Your Email",
/** @default "Please verify your email address. Check your inbox for the verification email. If you haven't received the email, click the button below to resend." */
verifyYourEmailDescription: "Please verify your email address. Check your inbox for the verification email. If you haven't received the email, click the button below to resend.",
/** @default "Go back" */
goBack: "Go back",
/** @default "Invalid password" */
invalidPassword: "Invalid password",
/** @default "Password too short" */
passwordTooShort: "Password too short",
// Admin-related messages
/** @default "Admin" */
admin: "Admin",
/** @default "Users" */
users: "Users",
/** @default "Create User" */
createUser: "Create User",
/** @default "Creating..." */
creatingUser: "Creating...",
/** @default "User created successfully" */
userCreatedSuccess: "User created successfully",
/** @default "Failed to create user" */
userCreatedError: "Failed to create user. Please try again.",
// Keep the existing nameRequired property as it's used elsewhere
/** @default "Name is required for admin user" */
adminNameRequired: "Name is required",
/** @default "Email is required for admin user" */
adminEmailRequired: "Email is required",
/** @default "Invalid email address for admin user" */
adminEmailInvalid: "Invalid email address",
/** @default "Password must be at least 8 characters" */
passwordMinLength: "Password must be at least 8 characters",
/** @default "Role is required" */
adminRoleRequired: "Role is required",
/** @default "User banned successfully" */
userBannedSuccess: "User has been banned successfully",
/** @default "Failed to ban user" */
userBannedError: "Failed to ban user. Please try again.",
/** @default "User unbanned successfully" */
userUnbannedSuccess: "User has been unbanned successfully",
/** @default "Failed to unban user" */
userUnbannedError: "Failed to unban user. Please try again.",
/** @default "User removed successfully" */
userRemovedSuccess: "User has been removed successfully",
/** @default "Failed to remove user" */
userRemovedError: "Failed to remove user. Please try again.",
/** @default "Failed to fetch users" */
fetchUsersError: "Failed to fetch users. Please try again."
};
// src/components/admin/create-user-dialog.tsx
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
function CreateUserDialog({
open,
onOpenChange,
onUserCreated,
classNames
}) {
const { authClient, toast: renderToast, phoneNumber: phoneNumberEnabled } = useContext2(AuthUIContext);
const localization = { ...authLocalization };
const schemaFields = {
name: z.string().min(1, { message: authLocalization.adminNameRequired }),
email: z.string().min(1, { message: authLocalization.adminEmailRequired }).email({
message: authLocalization.adminEmailInvalid
}),
password: z.string().min(8, {
message: authLocalization.passwordMinLength
})
};
if (phoneNumberEnabled) {
schemaFields.phoneNumber = z.string().min(1, {
message: `${localization.phoneNumber} ${localization.isRequired}`
}).regex(/^\+?[1-9]\d{1,14}$/, {
message: localization.phoneInvalid || "Please enter a valid phone number"
});
}
const formSchema = z.object(schemaFields);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
email: "",
password: "",
role: "user"
}
});
const isSubmitting = form.formState.isSubmitting;
useEffect2(() => {
if (!open) {
form.reset();
}
}, [open, form]);
async function createUser(values) {
try {
const roleValue = values.role;
await authClient.admin.createUser(
{
name: values.name,
email: values.email,
password: values.password,
role: roleValue,
...values.phoneNumber !== void 0 && { data: { phoneNumber: values.phoneNumber } }
},
{
onSuccess() {
renderToast({
variant: "success",
message: authLocalization.userCreatedSuccess
});
form.reset();
onOpenChange(false);
onUserCreated();
},
onError() {
renderToast({
variant: "error",
message: authLocalization.userCreatedError
});
form.resetField("password");
}
}
);
} catch (error) {
console.error("Failed to create user:", error);
renderToast({
variant: "error",
message: authLocalization.userCreatedError
});
form.resetField("password");
}
}
return /* @__PURE__ */ jsx2(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-[425px]", children: [
/* @__PURE__ */ jsxs(DialogHeader, { children: [
/* @__PURE__ */ jsx2(DialogTitle, { children: authLocalization.createUser }),
/* @__PURE__ */ jsx2(DialogDescription, { children: "Fill in the details to create a new user account." })
] }),
/* @__PURE__ */ jsx2(Form, { ...form, children: /* @__PURE__ */ jsxs(
"form",
{
onSubmit: form.handleSubmit(createUser),
className: "space-y-4 py-4",
children: [
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
/* @__PURE__ */ jsx2(
FormField,
{
control: form.control,
name: "name",
render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, { children: [
/* @__PURE__ */ jsx2(FormLabel, { children: "Name" }),
/* @__PURE__ */ jsx2(FormControl, { children: /* @__PURE__ */ jsx2(
Input,
{
placeholder: "John Doe",
disabled: isSubmitting,
...field
}
) }),
/* @__PURE__ */ jsx2(FormMessage, {})
] })
}
),
/* @__PURE__ */ jsx2(
FormField,
{
control: form.control,
name: "email",
render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, { children: [
/* @__PURE__ */ jsx2(FormLabel, { children: "Email" }),
/* @__PURE__ */ jsx2(FormControl, { children: /* @__PURE__ */ jsx2(
Input,
{
type: "email",
placeholder: "john@example.com",
disabled: isSubmitting,
...field
}
) }),
/* @__PURE__ */ jsx2(FormMessage, {})
] })
}
),
phoneNumberEnabled && /* @__PURE__ */ jsx2(
FormField,
{
control: form.control,
name: "phoneNumber",
render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, { children: [
/* @__PURE__ */ jsx2(FormLabel, { className: classNames?.label, children: localization.phoneNumber }),
/* @__PURE__ */ jsx2(FormControl, { children: /* @__PURE__ */ jsx2(
Input,
{
className: classNames?.input,
type: "tel",
placeholder: localization.phonePlaceholder,
disabled: isSubmitting,
...field
}
) }),
/* @__PURE__ */ jsx2("p", { className: "text-sm text-muted-foreground", children: localization.phoneDescription }),
/* @__PURE__ */ jsx2(FormMessage, { className: classNames?.error })
] })
}
),
/* @__PURE__ */ jsx2(
FormField,
{
control: form.control,
name: "password",
render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, { children: [
/* @__PURE__ */ jsx2(FormLabel, { children: "Password" }),
/* @__PURE__ */ jsx2(FormControl, { children: /* @__PURE__ */ jsx2(
Input,
{
type: "password",
placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
disabled: isSubmitting,
...field
}
) }),
/* @__PURE__ */ jsx2(FormMessage, {})
] })
}
),
/* @__PURE__ */ jsx2(
FormField,
{
control: form.control,
name: "role",
render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, { children: [
/* @__PURE__ */ jsx2(FormLabel, { children: "Role" }),
/* @__PURE__ */ jsx2(FormControl, { children: /* @__PURE__ */ jsx2(
Input,
{
placeholder: "user",
disabled: isSubmitting,
...field
}
) }),
/* @__PURE__ */ jsx2(FormMessage, {}),
/* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: 'Default is "user".' })
] })
}
)
] }),
/* @__PURE__ */ jsxs(DialogFooter, { children: [
/* @__PURE__ */ jsx2(
Button,
{
type: "button",
variant: "outline",
onClick: () => onOpenChange(false),
disabled: isSubmitting,
children: "Cancel"
}
),
/* @__PURE__ */ jsx2(Button, { type: "submit", disabled: isSubmitting, children: isSubmitting ? /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx2(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
authLocalization.creatingUser
] }) : authLocalization.createUser })
] })
]
}
) })
] }) });
}
// src/components/admin/pagination.tsx
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
function Pagination({
currentPage,
totalPages,
totalItems,
pageSize,
isLoading,
isActionPending,
className,
onPageChange
}) {
if (totalPages <= 1) return null;
const startIndex = (currentPage - 1) * pageSize + 1;
const endIndex = Math.min(currentPage * pageSize, totalItems);
return /* @__PURE__ */ jsxs2("div", { className: cn("flex justify-between items-center mt-4", className), children: [
/* @__PURE__ */ jsxs2("div", { children: [
"Showing ",
startIndex,
" to ",
endIndex,
" of ",
totalItems,
" users"
] }),
/* @__PURE__ */ jsxs2("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsx3(
Button,
{
variant: "outline",
size: "sm",
onClick: () => onPageChange(currentPage - 1),
disabled: currentPage === 1 || isLoading || isActionPending,
children: "Previous"
}
),
/* @__PURE__ */ jsx3(
Button,
{
variant: "outline",
size: "sm",
onClick: () => onPageChange(currentPage + 1),
disabled: currentPage === totalPages || isLoading || isActionPending,
children: "Next"
}
)
] })
] });
}
// src/components/ui/skeleton.tsx
import { jsx as jsx4 } from "react/jsx-runtime";
function Skeleton({ className, ...props }) {
return /* @__PURE__ */ jsx4(
"div",
{
"data-slot": "skeleton",
className: cn("bg-accent animate-pulse rounded-md", className),
...props
}
);
}
// src/components/ui/table.tsx
import * as React from "react";
import { jsx as jsx5 } from "react/jsx-runtime";
var Table = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsx5(
"table",
{
ref,
className: cn("w-full caption-bottom text-sm", className),
...props
}
) }));
Table.displayName = "Table";
var TableHeader = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5("thead", { ref, className: cn("[&_tr]:border-b", className), ...props }));
TableHeader.displayName = "TableHeader";
var TableBody = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5(
"tbody",
{
ref,
className: cn("[&_tr:last-child]:border-0", className),
...props
}
));
TableBody.displayName = "TableBody";
var TableFooter = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5(
"tfoot",
{
ref,
className: cn("bg-primary font-medium text-primary-foreground", className),
...props
}
));
TableFooter.displayName = "TableFooter";
var TableRow = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5(
"tr",
{
ref,
className: cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
),
...props
}
));
TableRow.displayName = "TableRow";
var TableHead = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5(
"th",
{
ref,
className: cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
),
...props
}
));
TableHead.displayName = "TableHead";
var TableCell = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5(
"td",
{
ref,
className: cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className),
...props
}
));
TableCell.displayName = "TableCell";
var TableCaption = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx5(
"caption",
{
ref,
className: cn("mt-4 text-sm text-muted-foreground", className),
...props
}
));
TableCaption.displayName = "TableCaption";
// src/components/admin/user-action-menu.tsx
import {
Ban,
Check,
Loader2 as Loader22,
LogOut,
MoreHorizontal,
Shield,
Trash2
} from "lucide-react";
import { useState as useState2 } from "react";
// src/components/ui/dropdown-menu.tsx
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
function DropdownMenu({
...props
}) {
return /* @__PURE__ */ jsx6(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
}
function DropdownMenuTrigger({
...props
}) {
return /* @__PURE__ */ jsx6(
DropdownMenuPrimitive.Trigger,
{
"data-slot": "dropdown-menu-trigger",
...props
}
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}) {
return /* @__PURE__ */ jsx6(DropdownMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx6(
DropdownMenuPrimitive.Content,
{
"data-slot": "dropdown-menu-content",
sideOffset,
className: cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
),
...props
}
) });
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}) {
return /* @__PURE__ */ jsx6(
DropdownMenuPrimitive.Item,
{
"data-slot": "dropdown-menu-item",
"data-inset": inset,
"data-variant": variant,
className: cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
),
...props
}
);
}
function DropdownMenuSeparator({
className,
...props
}) {
return /* @__PURE__ */ jsx6(
DropdownMenuPrimitive.Separator,
{
"data-slot": "dropdown-menu-separator",
className: cn("bg-border -mx-1 my-1 h-px", className),
...props
}
);
}
// src/components/admin/user-action-menu.tsx
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
function UserActionMenu({
userId,
status,
role,
disabled,
onUserAction
}) {
const [showMenu, setShowMenu] = useState2(false);
const [showRemoveDialog, setShowRemoveDialog] = useState2(false);
const [showRoleDialog, setShowRoleDialog] = useState2(false);
const [showRevokeDialog, setShowRevokeDialog] = useState2(false);
const [newRole, setNewRole] = useState2(
Array.isArray(role) ? role.join(", ") : role
);
const [isRoleActionPending, setIsRoleActionPending] = useState2(false);
const [isRevokeActionPending, setIsRevokeActionPending] = useState2(false);
const [isRemoveActionPending, setIsRemoveActionPending] = useState2(false);
const handleRoleChange = async () => {
if (!newRole.trim()) {
return;
}
setIsRoleActionPending(true);
try {
const roleValue = newRole.includes(",") ? newRole.split(",").map((r) => r.trim()).filter(Boolean) : newRole.trim();
const result = await onUserAction("setRole", userId, { role: roleValue });
if (result.success) {
setShowRoleDialog(false);
}
} finally {
setIsRoleActionPending(false);
}
};
const handleRevokeSessions = async () => {
setIsRevokeActionPending(true);
try {
const result = await onUserAction("revokeSessions", userId);
if (result.success) {
setShowRevokeDialog(false);
}
} finally {
setIsRevokeActionPending(false);
}
};
const handleRemoveUser = async () => {
setIsRemoveActionPending(true);
try {
const result = await onUserAction("remove", userId);
if (result.success) {
setShowRemoveDialog(false);
}
} finally {
setIsRemoveActionPending(false);
}
};
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
/* @__PURE__ */ jsxs4(DropdownMenu, { open: showMenu, onOpenChange: setShowMenu, children: [
/* @__PURE__ */ jsx7(DropdownMenuTrigger, { children: /* @__PURE__ */ jsxs4(
Button,
{
variant: "ghost",
size: "icon",
disabled,
onClick: () => setShowMenu(true),
children: [
/* @__PURE__ */ jsx7(MoreHorizontal, { className: "h-4 w-4" }),
/* @__PURE__ */ jsx7("span", { className: "sr-only", children: "Open menu" })
]
}
) }),
/* @__PURE__ */ jsxs4(DropdownMenuContent, { align: "end", children: [
status === "active" ? /* @__PURE__ */ jsxs4(
DropdownMenuItem,
{
onClick: () => onUserAction("ban", userId),
disabled,
children: [
/* @__PURE__ */ jsx7(Ban, { className: "h-4 w-4 mr-2" }),
"Ban"
]
}
) : /* @__PURE__ */ jsxs4(
DropdownMenuItem,
{
onClick: () => onUserAction("unban", userId),
disabled,
children: [
/* @__PURE__ */ jsx7(Check, { className: "h-4 w-4 mr-2" }),
"Unban"
]
}
),
/* @__PURE__ */ jsxs4(
DropdownMenuItem,
{
onClick: () => {
setShowMenu(false);
setShowRoleDialog(true);
},
disabled,
children: [
/* @__PURE__ */ jsx7(Shield, { className: "h-4 w-4 mr-2" }),
"Change Role"
]
}
),
/* @__PURE__ */ jsxs4(
DropdownMenuItem,
{
onClick: () => {
setShowMenu(false);
setShowRevokeDialog(true);
},
disabled,
children: [
/* @__PURE__ */ jsx7(LogOut, { className: "h-4 w-4 mr-2" }),
"Revoke Sessions"
]
}
),
/* @__PURE__ */ jsx7(DropdownMenuSeparator, {}),
/* @__PURE__ */ jsxs4(
DropdownMenuItem,
{
onClick: () => {
setShowMenu(false);
setShowRemoveDialog(true);
},
className: "text-red-600",
disabled,
children: [
/* @__PURE__ */ jsx7(Trash2, { className: "h-4 w-4 mr-2" }),
"Remove"
]
}
)
] })
] }),
/* @__PURE__ */ jsx7(Dialog, { open: showRemoveDialog, onOpenChange: setShowRemoveDialog, children: /* @__PURE__ */ jsxs4(
DialogContent,
{
className: "sm:max-w-[425px] ",
onOpenAutoFocus: (e) => e.preventDefault(),
children: [
/* @__PURE__ */ jsxs4(DialogHeader, { children: [
/* @__PURE__ */ jsx7(DialogTitle, { children: "Remove User" }),
/* @__PURE__ */ jsx7(DialogDescription, { children: "Are you sure you want to remove this user? This action cannot be undone." })
] }),
/* @__PURE__ */ jsxs4(DialogFooter, { children: [
/* @__PURE__ */ jsx7(
Button,
{
disabled: isRemoveActionPending,
variant: "outline",
onClick: () => !isRemoveActionPending && setShowRemoveDialog(false),
children: "Cancel"
}
),
/* @__PURE__ */ jsx7(
Button,
{
disabled: isRemoveActionPending,
variant: "destructive",
onClick: handleRemoveUser,
children: isRemoveActionPending ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
/* @__PURE__ */ jsx7(Loader22, { className: "mr-2 h-4 w-4 animate-spin" }),
"Removing