diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/tabs/user-overview-tab.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/tabs/user-overview-tab.tsx
index 7997bf9..e97e60a 100644
--- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/tabs/user-overview-tab.tsx
+++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/tabs/user-overview-tab.tsx
@@ -1,5 +1,4 @@
-// components/user-management/sheet/tabs/user-overview-tab.tsx
-import { Button } from "@/app/_components/ui/button";
+
import { Badge } from "@/app/_components/ui/badge";
import { Separator } from "@/app/_components/ui/separator";
import {
@@ -16,13 +15,30 @@ import { IUserSchema } from "@/src/entities/models/users/users.model";
import { formatDate } from "@/app/_utils/common";
import { CAlertDialog } from "@/app/_components/alert-dialog";
import { useUserDetailSheetHandlers } from "../../_handlers/use-detail-sheet";
+import { useGetCurrentUserQuery } from "../../_queries/queries";
+import { AuthenticationError } from "@/src/entities/errors/auth";
+import { useCheckPermissionsHandler } from "@/app/(pages)/(auth)/_handlers/use-check-permissions";
+import { useCallback } from "react";
+import { cn } from "@/app/_lib/utils";
+import { Section } from "@/app/_components/Section";
+import { InfoRow } from "@/app/_components/info-row";
+import { ProviderInfo } from "@/app/_components/provider-info";
+import { ActionRow } from "@/app/_components/action-row";
+import { DangerAction } from "@/app/_components/danger-action";
+
interface UserOverviewTabProps {
- user: IUserSchema;
+ selectedUser: IUserSchema;
handleCopyItem: (text: string, label: string) => void;
}
-export function UserOverviewTab({ user, handleCopyItem }: UserOverviewTabProps) {
+export function UserOverviewTab({ selectedUser, handleCopyItem }: UserOverviewTabProps) {
+ const { data: currentUser } = useGetCurrentUserQuery();
+
+ if (!currentUser) {
+ throw new AuthenticationError("Authentication error. Please log in again.");
+ }
+
const {
handleDeleteUser,
handleSendPasswordRecovery,
@@ -33,196 +49,160 @@ export function UserOverviewTab({ user, handleCopyItem }: UserOverviewTabProps)
isDeletePending,
isSendPasswordRecoveryPending,
isSendMagicLinkPending,
- } = useUserDetailSheetHandlers({ open: true, user, onOpenChange: () => { } });
+ } = useUserDetailSheetHandlers({ open: true, selectedUser, onOpenChange: () => { } });
+
+ const {
+ isAllowedToDelete,
+ isAllowedToBan,
+ isAllowedToSendPasswordRecovery,
+ isAllowedToSendMagicLink,
+ isAllowedToSendEmail,
+ } = useCheckPermissionsHandler(currentUser.email);
+
+ const memoizedHandleCopyItem = useCallback(
+ (text: string, label: string) => handleCopyItem(text, label),
+ [handleCopyItem]
+ );
return (
{/* User Information Section */}
-
-
User Information
-
-
-
-
User UID
-
- {user.id}
-
-
-
-
-
- Created at
- {formatDate(user.created_at)}
-
-
-
- Last signed in
- {formatDate(user.last_sign_in_at)}
-
-
-
- SSO
-
-
-
-
+
+ memoizedHandleCopyItem(selectedUser.id, "UID")}
+ />
+
+
+ }
+ />
+
{/* Provider Information Section */}
-
-
Provider Information
-
- The user has the following providers
-
-
-
-
-
-
-
-
Email
-
- Signed in with a email account
-
-
-
+
+ }
+ title="Email"
+ description="Signed in with an email account"
+ badge={
Enabled
-
-
+ }
+ />
-
-
-
-
Reset password
-
- Send a password recovery email to the user
-
-
-
-
+ {isAllowedToSendEmail && (
+
+ )}
-
+ {isAllowedToSendEmail && (
+
+ {isAllowedToSendPasswordRecovery && (
+
}
+ actionText="Send password recovery"
+ buttonVariant="outline"
+ buttonClassName="bg-secondary/80 border-secondary-foreground/20 hover:border-secondary-foreground/30"
+ />
+ )}
-
-
-
Send magic link
-
- Passwordless login via email for the user
-
-
-
+ {!isAllowedToSendMagicLink &&
}
+
+ {!isAllowedToSendMagicLink && (
+
}
+ actionText="Send magic link"
+ buttonVariant="outline"
+ buttonClassName="bg-secondary/80 border-secondary-foreground/20 hover:border-secondary-foreground/30"
+ />
+ )}
-
-
+ )}
+
{/* Danger Zone Section */}
-
-
- Danger zone
-
-
- Be wary of the following features as they cannot be undone.
-
-
-
-
-
-
Ban user
-
- Revoke access to the project for a set duration
-
-
-
-
-
-
-
-
Delete user
-
- User will no longer have access to the project
-
-
-
}
- title="Are you absolutely sure?"
- description="This action cannot be undone. This will permanently delete the user account and remove their data from our servers."
- confirmText="Delete"
- onConfirm={handleDeleteUser}
- isPending={isDeletePending}
- pendingText="Deleting..."
- variant="destructive"
- size="sm"
+ {isAllowedToDelete && (
+
+ {selectedUser.banned_until ? (
+ }
+ actionText="Unban user"
+ className="rounded-b-none"
+ buttonVariant="outline"
/>
-
-
-
+ ) : (
+
,
+ title: "Select ban duration",
+ description:
+ "The user will not be able to access the project for the selected duration.",
+ confirmText: "Ban",
+ onConfirm: handleToggleBan,
+ isPending: isBanPending,
+ pendingText: "Banning...",
+ variant: "outline",
+ size: "sm",
+ }}
+ />
+ )}
+
+
,
+ title: "Are you absolutely sure?",
+ description:
+ "This action cannot be undone. This will permanently delete the user account and remove their data from our servers.",
+ confirmText: "Delete",
+ onConfirm: handleDeleteUser,
+ isPending: isDeletePending,
+ pendingText: "Deleting...",
+ variant: "destructive",
+ size: "sm",
+ }}
+ />
+
+ )}
);
-}
\ No newline at end of file
+}
+
diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx
index 4856947..fdf33bf 100644
--- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx
+++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_components/user-management.tsx
@@ -4,7 +4,7 @@ import { useState, useMemo, useEffect } from "react";
import { DataTable } from "../../../../../_components/data-table";
import { UserInformationSheet } from "./sheets/user-information-sheet";
-import { useGetUsersQuery } from "../_queries/queries";
+import { useGetCurrentUserQuery, useGetUsersQuery } from "../_queries/queries";
import { filterUsers, useUserManagementHandlers } from "../_handlers/use-user-management";
import { useAddUserDialogHandler } from "../_handlers/use-add-user-dialog";
@@ -26,6 +26,8 @@ export default function UserManagement() {
refetch,
} = useGetUsersQuery();
+ const { data: currentUser } = useGetCurrentUserQuery()
+
// User management handler
const {
searchQuery,
@@ -72,6 +74,7 @@ export default function UserManagement() {
filters,
setFilters,
handleUserUpdate,
+ currentUser
)
// State untuk jumlah data di halaman saat ini
@@ -99,7 +102,7 @@ export default function UserManagement() {
{isDetailUser && (
diff --git a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-detail-sheet.ts b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-detail-sheet.ts
index bd3aa65..dddd7a2 100644
--- a/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-detail-sheet.ts
+++ b/sigap-website/app/(pages)/(admin)/dashboard/user-management/_handlers/use-detail-sheet.ts
@@ -7,9 +7,9 @@ import { copyItem } from "@/app/_utils/common";
import { useQueryClient } from "@tanstack/react-query";
import { useUserActionsHandler } from "./actions/use-user-actions";
-export const useUserDetailSheetHandlers = ({ open, user, onOpenChange }: {
+export const useUserDetailSheetHandlers = ({ open, selectedUser, onOpenChange }: {
open: boolean;
- user: IUserSchema;
+ selectedUser: IUserSchema;
onOpenChange: (open: boolean) => void;
}) => {
@@ -22,10 +22,10 @@ export const useUserDetailSheetHandlers = ({ open, user, onOpenChange }: {
const { mutateAsync: unbanUser, isPending: isUnbanPending } = useUnbanUserMutation();
const handleDeleteUser = async () => {
- await deleteUser(user.id, {
+ await deleteUser(selectedUser.id, {
onSuccess: () => {
invalidateUsers();
- toast.success(`${user.email} has been deleted`);
+ toast.success(`${selectedUser.email} has been deleted`);
onOpenChange(false);
}
@@ -33,10 +33,10 @@ export const useUserDetailSheetHandlers = ({ open, user, onOpenChange }: {
};
const handleSendPasswordRecovery = async () => {
- if (user.email) {
- await sendPasswordRecovery(user.email, {
+ if (selectedUser.email) {
+ await sendPasswordRecovery(selectedUser.email, {
onSuccess: () => {
- toast.success(`Password recovery email sent to ${user.email}`);
+ toast.success(`Password recovery email sent to ${selectedUser.email}`);
onOpenChange(false);
},
@@ -50,10 +50,10 @@ export const useUserDetailSheetHandlers = ({ open, user, onOpenChange }: {
}
const handleSendMagicLink = async () => {
- if (user.email) {
- await sendMagicLink(user.email, {
+ if (selectedUser.email) {
+ await sendMagicLink(selectedUser.email, {
onSuccess: () => {
- toast.success(`Magic link sent to ${user.email}`);
+ toast.success(`Magic link sent to ${selectedUser.email}`);
onOpenChange(false);
},
@@ -67,10 +67,10 @@ export const useUserDetailSheetHandlers = ({ open, user, onOpenChange }: {
};
const handleBanUser = async (ban_duration: ValidBanDuration = "24h") => {
- await banUser({ id: user.id, ban_duration: ban_duration }, {
+ await banUser({ id: selectedUser.id, ban_duration: ban_duration }, {
onSuccess: () => {
invalidateUsers();
- toast(`${user.email} has been banned`);
+ toast(`${selectedUser.email} has been banned`);
}
@@ -78,10 +78,10 @@ export const useUserDetailSheetHandlers = ({ open, user, onOpenChange }: {
};
const handleUnbanUser = async () => {
- await unbanUser({ id: user.id }, {
+ await unbanUser({ id: selectedUser.id }, {
onSuccess: () => {
invalidateUsers();
- toast(`${user.email} has been unbanned`);
+ toast(`${selectedUser.email} has been unbanned`);
}
@@ -89,21 +89,21 @@ export const useUserDetailSheetHandlers = ({ open, user, onOpenChange }: {
};
const handleToggleBan = async (ban_duration: ValidBanDuration = "24h") => {
- if (user.banned_until) {
- await unbanUser({ id: user.id }, {
+ if (selectedUser.banned_until) {
+ await unbanUser({ id: selectedUser.id }, {
onSuccess: () => {
invalidateUsers();
- toast(`${user.email} has been unbanned`);
+ toast(`${selectedUser.email} has been unbanned`);
}
});
} else {
- await banUser({ id: user.id, ban_duration: ban_duration }, {
+ await banUser({ id: selectedUser.id, ban_duration: ban_duration }, {
onSuccess: () => {
invalidateUsers();
- toast(`${user.email} has been banned`);
+ toast(`${selectedUser.email} has been banned`);
}
});
diff --git a/sigap-website/app/(pages)/(auth)/_handlers/use-check-permissions.ts b/sigap-website/app/(pages)/(auth)/_handlers/use-check-permissions.ts
index e69de29..5694d6e 100644
--- a/sigap-website/app/(pages)/(auth)/_handlers/use-check-permissions.ts
+++ b/sigap-website/app/(pages)/(auth)/_handlers/use-check-permissions.ts
@@ -0,0 +1,22 @@
+import { useCheckPermissionsQuery } from "../_queries/queries"
+
+export const useCheckPermissionsHandler = (email: string) => {
+
+ const { data: isAllowedToCreate } = useCheckPermissionsQuery(email, "users", "create")
+ const { data: isAllowedToUpdate } = useCheckPermissionsQuery(email, "users", "update")
+ const { data: isAllowedToDelete } = useCheckPermissionsQuery(email, "users", "delete")
+
+ const { data: isAllowedToBan } = useCheckPermissionsQuery(email, "users", "ban")
+ const { data: isAllowedToSendPasswordRecovery } = useCheckPermissionsQuery(email, "users", "send_password_recovery",)
+ const { data: isAllowedToSendMagicLink } = useCheckPermissionsQuery(email, "users", "send_magic_link")
+
+ return {
+ isAllowedToCreate,
+ isAllowedToUpdate,
+ isAllowedToDelete,
+ isAllowedToBan,
+ isAllowedToSendPasswordRecovery,
+ isAllowedToSendMagicLink,
+ isAllowedToSendEmail: isAllowedToSendPasswordRecovery || isAllowedToSendMagicLink,
+ }
+}
\ No newline at end of file
diff --git a/sigap-website/app/_components/action-row.tsx b/sigap-website/app/_components/action-row.tsx
new file mode 100644
index 0000000..4b559de
--- /dev/null
+++ b/sigap-website/app/_components/action-row.tsx
@@ -0,0 +1,66 @@
+import { ReactNode } from "react";
+import { Button } from "@/app/_components/ui/button";
+import { Loader2 } from "lucide-react";
+import { cn } from "@/app/_lib/utils";
+
+interface ActionRowProps {
+ title: string;
+ description: string;
+ onClick: () => void;
+ isPending?: boolean;
+ pendingText?: string;
+ icon?: ReactNode;
+ actionText: string;
+ className?: string;
+ contentClassName?: string;
+ titleClassName?: string;
+ descriptionClassName?: string;
+ buttonClassName?: string;
+ buttonVariant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
+ buttonSize?: "default" | "sm" | "lg" | "icon";
+}
+
+export function ActionRow({
+ title,
+ description,
+ onClick,
+ isPending = false,
+ pendingText = "Loading...",
+ icon,
+ actionText,
+ className,
+ contentClassName,
+ titleClassName,
+ descriptionClassName,
+ buttonClassName,
+ buttonVariant = "outline",
+ buttonSize = "sm"
+}: ActionRowProps) {
+ return (
+
+
+
{title}
+
{description}
+
+
+
+ );
+}
diff --git a/sigap-website/app/_components/danger-action.tsx b/sigap-website/app/_components/danger-action.tsx
new file mode 100644
index 0000000..7a27d9f
--- /dev/null
+++ b/sigap-website/app/_components/danger-action.tsx
@@ -0,0 +1,79 @@
+import { ReactNode } from "react";
+import { Button } from "@/app/_components/ui/button";
+import { Loader2 } from "lucide-react";
+import { CAlertDialog } from "@/app/_components/alert-dialog";
+import { cn } from "@/app/_lib/utils";
+
+interface DangerActionProps {
+ title: string;
+ description: string;
+ onClick?: () => void;
+ isPending?: boolean;
+ pendingText?: string;
+ icon?: ReactNode;
+ actionText?: string;
+ isDialog?: boolean;
+ dialogProps?: any;
+ className?: string;
+ contentClassName?: string;
+ titleClassName?: string;
+ descriptionClassName?: string;
+ buttonClassName?: string;
+ containerClassName?: string;
+ buttonVariant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
+ buttonSize?: "default" | "sm" | "lg" | "icon";
+}
+
+export function DangerAction({
+ title,
+ description,
+ onClick,
+ isPending = false,
+ pendingText = "Loading...",
+ icon,
+ actionText,
+ isDialog,
+ dialogProps,
+ className,
+ contentClassName,
+ titleClassName,
+ descriptionClassName,
+ buttonClassName,
+ containerClassName,
+ buttonVariant = "outline",
+ buttonSize = "sm"
+}: DangerActionProps) {
+ return (
+
+
+
{title}
+
{description}
+
+ {isDialog ? (
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/sigap-website/app/_components/info-row.tsx b/sigap-website/app/_components/info-row.tsx
new file mode 100644
index 0000000..10e4e29
--- /dev/null
+++ b/sigap-website/app/_components/info-row.tsx
@@ -0,0 +1,45 @@
+import { ReactNode } from "react";
+import { Button } from "@/app/_components/ui/button";
+import { Copy } from "lucide-react";
+import { cn } from "@/app/_lib/utils";
+
+interface InfoRowProps {
+ label: string;
+ value: ReactNode;
+ onCopy?: () => void;
+ className?: string;
+ labelClassName?: string;
+ valueClassName?: string;
+ copyButtonClassName?: string;
+}
+
+export function InfoRow({
+ label,
+ value,
+ onCopy,
+ className,
+ labelClassName,
+ valueClassName,
+ copyButtonClassName
+}: InfoRowProps) {
+ return (
+
+
{label}
+
+ {typeof value === "string" ? (
+ {value}
+ ) : value}
+ {onCopy && (
+
+ )}
+
+
+ );
+}
diff --git a/sigap-website/app/_components/provider-info.tsx b/sigap-website/app/_components/provider-info.tsx
new file mode 100644
index 0000000..7d08ff0
--- /dev/null
+++ b/sigap-website/app/_components/provider-info.tsx
@@ -0,0 +1,47 @@
+import { ReactNode } from "react";
+import { cn } from "@/app/_lib/utils";
+
+interface ProviderInfoProps {
+ icon: ReactNode;
+ title: string;
+ description: string;
+ badge: ReactNode;
+ className?: string;
+ containerClassName?: string;
+ headerClassName?: string;
+ titleContainerClassName?: string;
+ titleClassName?: string;
+ descriptionClassName?: string;
+ badgeClassName?: string;
+}
+
+export function ProviderInfo({
+ icon,
+ title,
+ description,
+ badge,
+ className,
+ containerClassName,
+ headerClassName,
+ titleContainerClassName,
+ titleClassName,
+ descriptionClassName,
+ badgeClassName
+}: ProviderInfoProps) {
+ return (
+
+
+
+ {icon}
+
+
{title}
+
{description}
+
+
+
+ {badge}
+
+
+
+ );
+}
diff --git a/sigap-website/app/_components/section.tsx b/sigap-website/app/_components/section.tsx
new file mode 100644
index 0000000..bd8c47a
--- /dev/null
+++ b/sigap-website/app/_components/section.tsx
@@ -0,0 +1,34 @@
+import { ReactNode } from "react";
+import { cn } from "@/app/_lib/utils";
+
+interface SectionProps {
+ title: string;
+ description?: string;
+ children: ReactNode;
+ className?: string;
+ titleClassName?: string;
+ descriptionClassName?: string;
+ contentClassName?: string;
+}
+
+export function Section({
+ title,
+ description,
+ children,
+ className,
+ titleClassName,
+ descriptionClassName,
+ contentClassName
+}: SectionProps) {
+ return (
+
+
+
{title}
+ {description &&
{description}
}
+
+
+ {children}
+
+
+ );
+}
diff --git a/sigap-website/app/_styles/globals.css b/sigap-website/app/_styles/globals.css
index f3d43b9..ff7bfeb 100644
--- a/sigap-website/app/_styles/globals.css
+++ b/sigap-website/app/_styles/globals.css
@@ -84,6 +84,13 @@
/* Sekunder: abu-abu gelap untuk elemen pendukung */
--secondary: 0 0% 15%; /* #262626 */
--secondary-foreground: 0 0% 85%; /* #d9d9d9 */
+ --secondary-tertiary: #242424; /* #242424 */
+
+ /* Third */
+ --tertiary: 0 0% 12%; /* #1F1F1F1 */
+ --tertiary-foreground: 0 0% 85%; /* #d9d9d9 */
+ --tertiary-border: 0 0% 20%; /* #333333 */
+
/* Muted: abu-abu gelap untuk teks pendukung */
--muted: 0 0% 20%; /* #333333 */
diff --git a/sigap-website/app/_utils/common.ts b/sigap-website/app/_utils/common.ts
index 7456ff1..0e32710 100644
--- a/sigap-website/app/_utils/common.ts
+++ b/sigap-website/app/_utils/common.ts
@@ -278,8 +278,8 @@ export const formatDateWithLocaleAndFallback = (
* @param lastName - The last name.
* @returns The full name or "User" if both names are empty.
*/
-export const getFullName = (firstName: string, lastName: string): string => {
- return `${firstName} ${lastName}`.trim() || "User";
+export const getFullName = (firstName: string | null | undefined, lastName: string | null | undefined): string => {
+ return `${firstName || ""} ${lastName || ""}`.trim() || "User";
}
/**
diff --git a/sigap-website/app/_utils/const/roles.ts b/sigap-website/app/_utils/const/roles.ts
new file mode 100644
index 0000000..b04e98b
--- /dev/null
+++ b/sigap-website/app/_utils/const/roles.ts
@@ -0,0 +1,16 @@
+import { IUserRoles } from "@/src/entities/models/roles/roles.model";
+
+export const USER_ROLES = {
+
+ TYPES: {
+ ADMIN: 'admin',
+ STAFF: 'staff',
+ VIEWER: 'viewer',
+ },
+
+ ALLOWED_ROLES_TO_ACTIONS: [
+ "admin",
+ "staff"
+ ]
+
+};
diff --git a/sigap-website/src/entities/models/roles/roles.model.ts b/sigap-website/src/entities/models/roles/roles.model.ts
index 90d81d2..94dd10b 100644
--- a/sigap-website/src/entities/models/roles/roles.model.ts
+++ b/sigap-website/src/entities/models/roles/roles.model.ts
@@ -1,5 +1,18 @@
import { z } from "zod";
+export type IUserRoles = {
+ TYPES: {
+ ADMIN: 'admin';
+ STAFF: 'staff';
+ VIEWER: 'viewer';
+ };
+
+ ALLOWED_ROLES_TO_ACTIONS: [
+ 'admin',
+ 'staff'
+ ];
+};
+
export const RoleSchema = z.object({
id: z.string(),
name: z.string(),