diff --git a/messages/en.json b/messages/en.json index 0a48f32..e355932 100644 --- a/messages/en.json +++ b/messages/en.json @@ -24,6 +24,19 @@ } } ], + "exp_time": [ + { + "match": { + "time=1": "1 day", + "time=7": "7 days", + "time=15": "15 days", + "time=30": "1 month", + "time=180": "6 month", + "time=365": "1 year", + "time=99999": "Forever" + } + } + ], "ui_login_btn": "Sign in", "ui_logout_btn": "Sign out", "ui_cancel_btn": "Cancel", @@ -31,11 +44,14 @@ "ui_confirm_btn": "Confirm", "ui_signup_btn": "Sign up", "ui_view_btn": "View", - "ui_save_btn": "Save", + "ui_save_btn": "Save changes", "ui_update_btn": "Update", "ui_delete_btn": "Delete", "ui_ban_btn": "Lock", "ui_unban_btn": "Unlock", + "ui_update_password_btn": "Set password", + "ui_change_role_btn": "Set role", + "ui_edit_user_btn": "Edit User", "ui_dialog_view_title": "View {type} details", "ui_view_all_notifications": "View All Notifications", "ui_label_notifications": "Notifications", @@ -46,7 +62,8 @@ "nav_add_new": "Add new", "nav_edit": "Edit", "nav_change_password": "Change password", - "nav_log": "Logs", + "nav_logs": "Logs", + "nav_users": "Người dùng", "nav_roles": "Vai trò & quyền hạn", "nav_box": "Box", "nav_account": "Account", @@ -84,6 +101,9 @@ "logs_page_ui_table_header_table": "Table", "logs_page_ui_table_header_action": "Action", "logs_page_ui_table_header_create_at": "Create date", + "logs_page_ui_table_header_old_value": "Old value", + "logs_page_ui_table_header_new_value": "New value", + "logs_page_ui_table_header_record_id": "Record ID", "logs_page_ui_badge_action": [ { "match": { @@ -94,10 +114,47 @@ "action=unban": "Unlock", "action=sign_in": "Sign in", "action=sign_out": "Sign out", + "action=change_password": "Change password", "action=*": "Other" } } ], - "backend_INVALID_EMAIL_OR_PASSWORD": "Email or password incorrect!", - "backend_INVALID_PASSWORD": "Password incorrect!" + "users_page_ui_title": "Users", + "users_page_ui_table_header_name": "Name", + "users_page_ui_table_header_email": "Email", + "users_page_ui_table_header_role": "Role", + "users_page_ui_table_header_banned": "Banned?", + "users_page_ui_table_header_created_at": "Create date", + "users_page_message_user_not_found": "User not found!", + "users_page_message_user_min": "Password must be at least 5 characters long!", + "users_page_message_contain_uppercase": "Password must contain at least one UPPERCASE letter!", + "users_page_message_contain_lowercase": "Password must contain at least one lowercase letter!", + "users_page_message_contain_number": "Password must contain at least one number!", + "users_page_message_contain_special": "Password must contain at least one special character!", + "users_page_message_set_password_success": "Set password successfully!", + "users_page_message_update_info_success": "Edited user information successfully!", + "users_page_message_set_role_success": "Modified user role successfully!", + "users_page_message_banned_success": "Banned {name} successfully!", + "users_page_message_unbanned_success": "Unbanned {name} successfully!", + "users_page_message_created_user_success": "Created user successfully!", + "users_page_message_select_min_one_day": "Please select expiration at least 1 day!", + "users_page_message_role_select": "Must select one of them!", + "users_page_ui_form_ban_reason": "Ban reason", + "users_page_ui_form_ban_exp": "Ban expiration", + "users_page_ui_select_placeholder_role": "Select role", + "users_page_ui_select_placeholder_ban_exp": "Select time", + "users_page_ui_dialog_alert_title": "Unban this user?", + "users_page_ui_dialog_alert_ban_title": "", + "users_page_ui_dialog_alert_description": "Detail: \nName: {name}. \nEmail: {email}", + "users_page_ui_dialog_alert_description_2": "Reason: {reason}. \nExpiration: {exp}", + "backend_message": [ + { + "match": { + "code=INVALID_EMAIL_OR_PASSWORD": "Email or password incorrect!", + "code=INVALID_PASSWORD": "Password incorrect!", + "code=YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role", + "code=USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "Email already exists. Please choose another email!" + } + } + ] } diff --git a/messages/vi.json b/messages/vi.json index 3329bda..b76505f 100644 --- a/messages/vi.json +++ b/messages/vi.json @@ -24,6 +24,19 @@ } } ], + "exp_time": [ + { + "match": { + "time=1": "1 ngày", + "time=7": "7 ngày", + "time=15": "15 ngày", + "time=30": "1 tháng", + "time=180": "6 tháng", + "time=365": "1 năm", + "time=99999": "Vĩnh viễn" + } + } + ], "ui_login_btn": "Đăng nhập", "ui_logout_btn": "Đăng xuất", "ui_cancel_btn": "Hủy", @@ -31,11 +44,14 @@ "ui_confirm_btn": "Xác nhận", "ui_signup_btn": "Đăng ký", "ui_view_btn": "Xem", - "ui_save_btn": "Lưu", + "ui_save_btn": "Lưu thay đổi", "ui_update_btn": "Cập nhật", "ui_delete_btn": "Xóa", "ui_ban_btn": "Khóa", "ui_unban_btn": "Mở khóa", + "ui_update_password_btn": "Đặt lại mật khẩu", + "ui_change_role_btn": "Đặt lại quyền hạn", + "ui_edit_user_btn": "Chỉnh sửa người dùng", "ui_dialog_view_title": "Xem chi tiết {type}", "ui_view_all_notifications": "Xem tất cả thông báo", "ui_label_notifications": "Thông báo", @@ -46,7 +62,8 @@ "nav_add_new": "Thêm mới", "nav_edit": "Chỉnh sửa", "nav_change_password": "Đổi mật khẩu", - "nav_log": "Lịch sử", + "nav_logs": "Lịch sử", + "nav_users": "Người dùng", "nav_roles": "Vai trò & quyền hạn", "nav_box": "Hộp chứa", "nav_account": "Tài khoản", @@ -86,6 +103,7 @@ "logs_page_ui_table_header_create_at": "Ngày tạo", "logs_page_ui_table_header_old_value": "Giá trị cũ", "logs_page_ui_table_header_new_value": "Giá trị mới", + "logs_page_ui_table_header_record_id": "Id tương ứng", "logs_page_ui_badge_action": [ { "match": { @@ -96,10 +114,47 @@ "action=unban": "Mở khóa", "action=sign_in": "Đăng nhập", "action=sign_out": "Đăng xuất", + "action=change_password": "Đổi mật khẩu", "action=*": "Khác" } } ], - "backend_INVALID_EMAIL_OR_PASSWORD": "Email hoặc mật khẩu không đúng!", - "backend_INVALID_PASSWORD": "Mật khẩu hiện tại không đúng!" + "users_page_ui_title": "Người dùng", + "users_page_ui_table_header_name": "Tên", + "users_page_ui_table_header_email": "Email", + "users_page_ui_table_header_role": "Quyền hạn", + "users_page_ui_table_header_banned": "Đã khóa?", + "users_page_ui_table_header_created_at": "Ngày tạo", + "users_page_message_user_not_found": "Không tìm thấy người dùng này!", + "users_page_message_user_min": "Mật khẩu phải có ít nhất 5 ký tự!", + "users_page_message_contain_uppercase": "Mật khẩu phải có ít nhất 1 ký tự viết HOA!", + "users_page_message_contain_lowercase": "Mật khẩu phải có ít nhất 1 ký tự viết thường!", + "users_page_message_contain_number": "Mật khẩu phải có ít nhất 1 chữ số!", + "users_page_message_contain_special": "Mật khẩu phải có ít nhất 1 ký tự đặc biệt!", + "users_page_message_set_password_success": "Đặt lại mật khẩu thành công!", + "users_page_message_update_info_success": "Chỉnh sửa thông tin người dùng thành công!", + "users_page_message_set_role_success": "Chỉnh sửa quyền hạn người dùng thành công!", + "users_page_message_banned_success": "Người dùng {name} bị cấm thành công!", + "users_page_message_unbanned_success": "Người dùng {name} bỏ cấm thành công!", + "users_page_message_created_user_success": "Đăng ký người dùng thành công!", + "users_page_message_select_min_one_day": "Chọn ít nhất là 1 ngày!", + "users_page_message_role_select": "Mời bạn chọn quyền hạn!", + "users_page_ui_form_ban_reason": "Lý do cấm", + "users_page_ui_form_ban_exp": "Thời gian cấm", + "users_page_ui_select_placeholder_role": "Hãy chọn quyền hạn", + "users_page_ui_select_placeholder_ban_exp": "Hãy chọn thời gian cấm", + "users_page_ui_dialog_alert_title": "Bạn muốn mở khóa người dùng này?", + "users_page_ui_dialog_alert_ban_title": "Bạn muốn khóa người dùng này?", + "users_page_ui_dialog_alert_description": "Chi tiết: \nTên: {name}. \nEmail: {email}", + "users_page_ui_dialog_alert_description_2": "\nLý do: {reason}. \nHiệu lực: {exp}", + "backend_message": [ + { + "match": { + "code=INVALID_EMAIL_OR_PASSWORD": "Email hoặc mật khẩu không đúng!", + "code=INVALID_PASSWORD": "Mật khẩu hiện tại không đúng!", + "code=YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "Bạn không đủ quyền để chỉnh sửa quyền hạn người dùng!", + "code=USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "Email này đã có người sử dụng. Vui lòng chọn một email khác!" + } + } + ] } diff --git a/prisma/seed.ts b/prisma/seed.ts index a4f34eb..218ded9 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -25,7 +25,7 @@ async function main() { body: { email: 'luu.dat.tham@gmail.com', password: 'Th@m!S@m!040390', - name: 'Sam', + name: 'Sam Liu', role: 'admin', }, }); @@ -38,8 +38,8 @@ async function main() { ...settingsData, { key: admin ? (admin?.user?.id as string) : (mailExists?.id as string), - value: '{ "language": "en" }', - description: 'User Settings', + value: '{"language": "en"}', + description: 'User settings', relation: 'user', }, ]; @@ -49,20 +49,6 @@ async function main() { skipDuplicates: true, }); console.log('---------------Created settings-----------------'); - - // // Clear existing todos - // await prisma.todo.deleteMany() - - // // Create example todos - // const todos = await prisma.todo.createMany({ - // data: [ - // { title: 'Buy groceries' }, - // { title: 'Read a book' }, - // { title: 'Workout' }, - // ], - // }) - - // console.log(`✅ Created ${todos.count} todos`) } main() diff --git a/src/components/DisplayBreakLineMessage.tsx b/src/components/DisplayBreakLineMessage.tsx new file mode 100644 index 0000000..dd1ad6f --- /dev/null +++ b/src/components/DisplayBreakLineMessage.tsx @@ -0,0 +1,9 @@ +const DisplayBreakLineMessage = ({ + children, +}: { + children: React.ReactNode; +}) => { + return
{children}
; +}; + +export default DisplayBreakLineMessage; diff --git a/src/components/audit/action-badge.tsx b/src/components/audit/action-badge.tsx index 6fc5a6a..e2bc009 100644 --- a/src/components/audit/action-badge.tsx +++ b/src/components/audit/action-badge.tsx @@ -1,4 +1,5 @@ import { m } from '@/paraglide/messages'; +import { LOG_ACTION } from '@/types/enum'; import { Badge } from '../ui/badge'; export type UserActionType = { @@ -9,9 +10,10 @@ export type UserActionType = { unban: string; sign_in: string; sign_out: string; + change_password: string; }; -const ActionBadge = ({ action }: { action: keyof UserActionType }) => { +const ActionBadge = ({ action }: { action: LOG_ACTION }) => { const USER_ACTION = Object.freeze( new Proxy( { @@ -22,6 +24,7 @@ const ActionBadge = ({ action }: { action: keyof UserActionType }) => { sign_out: 'bg-yellow-400', ban: 'bg-rose-400', unban: 'bg-emerald-400', + change_password: 'bg-red-600', } as UserActionType, { get: function (target: UserActionType, name: string | symbol) { diff --git a/src/components/audit/audit-columns.tsx b/src/components/audit/audit-columns.tsx index ed6341c..299991d 100644 --- a/src/components/audit/audit-columns.tsx +++ b/src/components/audit/audit-columns.tsx @@ -1,24 +1,15 @@ import { m } from '@/paraglide/messages'; import { formatters } from '@/utils/formatters'; -import { jsonSupport } from '@/utils/help'; -import { EyeIcon } from '@phosphor-icons/react'; import { ColumnDef } from '@tanstack/react-table'; import { Badge } from '../ui/badge'; -import { Button } from '../ui/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '../ui/dialog'; -import { Label } from '../ui/label'; -import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; -import ActionBadge, { UserActionType } from './action-badge'; -export const logColumns: ColumnDef[] = [ +import { LOG_ACTION } from '@/types/enum'; +import ActionBadge from './action-badge'; +import ViewDetail from './view-detail-dialog'; + +export const logColumns: ColumnDef[] = [ { - accessorKey: 'user.name', + accessorFn: (row) => row.user?.name ?? '', header: m.logs_page_ui_table_header_username(), meta: { thClass: 'w-1/6', @@ -45,14 +36,12 @@ export const logColumns: ColumnDef[] = [ thClass: 'w-1/6', }, cell: ({ row }) => { - return ( - - ); + return ; }, }, { accessorKey: 'createdAt', - header: m.logs_page_ui_table_header_action(), + header: m.logs_page_ui_table_header_create_at(), meta: { thClass: 'w-2/6', }, @@ -72,88 +61,3 @@ export const logColumns: ColumnDef[] = [ ), }, ]; - -type ViewDetailProps = { - data: AuditLog; -}; - -const ViewDetail = ({ data }: ViewDetailProps) => { - return ( - - - - - - - - - - - - - - - {m.ui_dialog_view_title({ type: m.nav_log() })} - - -
-
- - {m.logs_page_ui_table_header_username()}: - - -
-
- - {m.logs_page_ui_table_header_table()}: - - - {data.tableName} - -
-
- - {m.logs_page_ui_table_header_action()}: - - -
- {data.oldValue && ( -
- - {m.logs_page_ui_table_header_old_value()}: - -
-                {jsonSupport(data.oldValue)}
-              
-
- )} -
- - {m.logs_page_ui_table_header_new_value()}: - -
-              {data.newValue ? jsonSupport(data.newValue) : ''}
-            
-
-
- - {m.logs_page_ui_table_header_create_at()}: - - {formatters.dateTime(new Date(data.createdAt))} -
-
-
-
- ); -}; diff --git a/src/components/audit/view-detail-dialog.tsx b/src/components/audit/view-detail-dialog.tsx new file mode 100644 index 0000000..7606448 --- /dev/null +++ b/src/components/audit/view-detail-dialog.tsx @@ -0,0 +1,137 @@ +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'; +import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus'; +import { m } from '@/paraglide/messages'; +import { LOG_ACTION } from '@/types/enum'; +import { formatters } from '@/utils/formatters'; +import { jsonSupport } from '@/utils/helper'; +import { CheckIcon, CopyIcon, EyeIcon } from '@phosphor-icons/react'; +import { Badge } from '../ui/badge'; +import { Button } from '../ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../ui/dialog'; +import { Label } from '../ui/label'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; +import ActionBadge from './action-badge'; + +type ViewDetailProps = { + data: AuditWithUser; +}; + +const ViewDetail = ({ data }: ViewDetailProps) => { + const prevent = usePreventAutoFocus(); + const { isCopied, copyToClipboard } = useCopyToClipboard(); + + return ( + + + + + + + + + + + + e.preventDefault()} + > + + + + {m.ui_dialog_view_title({ type: m.nav_logs() })} + + + {m.ui_dialog_view_title({ type: m.nav_logs() })} + + +
+
+ + {m.logs_page_ui_table_header_username()}: + + +
+
+ + {m.logs_page_ui_table_header_table()}: + + + {data.tableName} + +
+
+ + {m.logs_page_ui_table_header_action()}: + + +
+ {data.oldValue && ( +
+ + {m.logs_page_ui_table_header_old_value()}: + +
+                {jsonSupport(data.oldValue)}
+              
+
+ )} + {data.newValue && ( +
+ + {m.logs_page_ui_table_header_new_value()}: + +
+                {data.newValue ? jsonSupport(data.newValue) : ''}
+              
+
+ )} +
+ + {m.logs_page_ui_table_header_record_id()}: + +
+ {data.recordId} + +
+
+
+ + {m.logs_page_ui_table_header_create_at()}: + + {formatters.dateTime(new Date(data.createdAt))} +
+
+
+
+ ); +}; + +export default ViewDetail; diff --git a/src/components/form/admin-ban-user-form.tsx b/src/components/form/admin-ban-user-form.tsx new file mode 100644 index 0000000..815ba55 --- /dev/null +++ b/src/components/form/admin-ban-user-form.tsx @@ -0,0 +1,172 @@ +import { m } from '@/paraglide/messages'; +import { userBanSchema } from '@/service/user.schema'; +import { WarningIcon } from '@phosphor-icons/react'; +import { useForm } from '@tanstack/react-form'; +import { UserWithRole } from 'better-auth/plugins'; +import { Alert, AlertDescription, AlertTitle } from '../ui/alert'; +import { Button } from '../ui/button'; +import { DialogClose, DialogFooter } from '../ui/dialog'; +import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field'; +import { Input } from '../ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../ui/select'; +import { Textarea } from '../ui/textarea'; +import { useBanContext } from '../user/ban-user-dialog'; + +type FormProps = { + data: UserWithRole; +}; + +const BanUserForm = ({ data }: FormProps) => { + const { setSubmitData, setOpen, setOpenConfirm } = useBanContext(); + + const form = useForm({ + defaultValues: { + id: data.id, + banReason: '', + banExp: 0, + }, + validators: { + onChange: userBanSchema, + onSubmit: userBanSchema, + }, + onSubmit: async ({ value }) => { + setSubmitData(value); + setOpen(false); + setTimeout(() => { + setOpenConfirm(true); + }, 100); + }, + }); + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + + + + {m.profile_form_name()}: {data.name} + + adá + + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + + {isInvalid && } + + ); + }} + /> + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + + {m.users_page_ui_form_ban_reason()}: + +