diff --git a/messages/en.json b/messages/en.json index 019ede7..e355932 100644 --- a/messages/en.json +++ b/messages/en.json @@ -103,6 +103,7 @@ "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": { @@ -113,6 +114,7 @@ "action=unban": "Unlock", "action=sign_in": "Sign in", "action=sign_out": "Sign out", + "action=change_password": "Change password", "action=*": "Other" } } @@ -134,7 +136,9 @@ "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", @@ -148,7 +152,8 @@ "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=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 534b059..b76505f 100644 --- a/messages/vi.json +++ b/messages/vi.json @@ -103,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": { @@ -113,6 +114,7 @@ "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" } } @@ -134,7 +136,9 @@ "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", @@ -148,7 +152,8 @@ "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=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 c7bb214..218ded9 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -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/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 6aa7650..299991d 100644 --- a/src/components/audit/audit-columns.tsx +++ b/src/components/audit/audit-columns.tsx @@ -3,7 +3,8 @@ import { formatters } from '@/utils/formatters'; import { ColumnDef } from '@tanstack/react-table'; import { Badge } from '../ui/badge'; -import ActionBadge, { UserActionType } from './action-badge'; +import { LOG_ACTION } from '@/types/enum'; +import ActionBadge from './action-badge'; import ViewDetail from './view-detail-dialog'; export const logColumns: ColumnDef[] = [ @@ -35,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', }, diff --git a/src/components/audit/view-detail-dialog.tsx b/src/components/audit/view-detail-dialog.tsx index e001f14..7606448 100644 --- a/src/components/audit/view-detail-dialog.tsx +++ b/src/components/audit/view-detail-dialog.tsx @@ -1,8 +1,10 @@ +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 { EyeIcon } from '@phosphor-icons/react'; +import { CheckIcon, CopyIcon, EyeIcon } from '@phosphor-icons/react'; import { Badge } from '../ui/badge'; import { Button } from '../ui/button'; import { @@ -15,7 +17,7 @@ import { } from '../ui/dialog'; import { Label } from '../ui/label'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; -import ActionBadge, { UserActionType } from './action-badge'; +import ActionBadge from './action-badge'; type ViewDetailProps = { data: AuditWithUser; @@ -23,6 +25,7 @@ type ViewDetailProps = { const ViewDetail = ({ data }: ViewDetailProps) => { const prevent = usePreventAutoFocus(); + const { isCopied, copyToClipboard } = useCopyToClipboard(); return ( @@ -80,7 +83,7 @@ const ViewDetail = ({ data }: ViewDetailProps) => { {m.logs_page_ui_table_header_action()}: - + {data.oldValue && (
@@ -92,13 +95,32 @@ const ViewDetail = ({ data }: ViewDetailProps) => {
)} -
+ {data.newValue && ( +
+ + {m.logs_page_ui_table_header_new_value()}: + +
+                {data.newValue ? jsonSupport(data.newValue) : ''}
+              
+
+ )} +
- {m.logs_page_ui_table_header_new_value()}: + {m.logs_page_ui_table_header_record_id()}: -
-              {data.newValue ? jsonSupport(data.newValue) : ''}
-            
+
+ {data.recordId} + +
diff --git a/src/components/form/admin-create-user-form.tsx b/src/components/form/admin-create-user-form.tsx new file mode 100644 index 0000000..0f8cfd4 --- /dev/null +++ b/src/components/form/admin-create-user-form.tsx @@ -0,0 +1,198 @@ +import { m } from '@/paraglide/messages'; +import { usersQueries } from '@/service/queries'; +import { createUser } from '@/service/user.api'; +import { RoleEnum, userCreateSchema } from '@/service/user.schema'; +import { ReturnError } from '@/types/common'; +import { useForm } from '@tanstack/react-form'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; +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'; + +type FormProps = { + onSubmit: (open: boolean) => void; +}; + +const AdminCreateUserForm = ({ onSubmit }: FormProps) => { + const queryClient = useQueryClient(); + + const { mutate: createUserMutation } = useMutation({ + mutationFn: createUser, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [...usersQueries.all, 'list'], + }); + onSubmit(false); + toast.success(m.users_page_message_created_user_success(), { + richColors: true, + }); + }, + onError: (error: ReturnError) => { + console.error(error); + toast.error(m.backend_message({ code: error.code }), { + richColors: true, + }); + }, + }); + + const form = useForm({ + defaultValues: { + email: '', + password: '', + name: '', + role: '', + }, + validators: { + onChange: userCreateSchema, + onSubmit: userCreateSchema, + }, + onSubmit: ({ value }) => { + createUserMutation({ data: userCreateSchema.parse(value) }); + }, + }); + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + + {m.login_page_form_email()}: + + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + type="email" + /> + {isInvalid && } + + ); + }} + /> + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + + {m.login_page_form_password()} + + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + type="password" + /> + {isInvalid && } + + ); + }} + /> + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + + {m.profile_form_name()} + + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + type="text" + /> + {isInvalid && } + + ); + }} + /> + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + + {m.profile_form_role()} + + + {isInvalid && } + + ); + }} + /> + + + + + + + + + +
+ ); +}; + +export default AdminCreateUserForm; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 386e33b..d1eed66 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - "focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 disabled:data-[active=true]:opacity-100 disabled:data-[active=true]:bg-teal-400 disabled:data-[active=true]:border-teal-400 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", + "focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-[2px] aria-invalid:ring-[2px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 disabled:data-[active=true]:opacity-100 disabled:data-[active=true]:bg-primary disabled:data-[active=true]:border-primary disabled:data-[active=true]:text-white [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", { variants: { variant: { diff --git a/src/components/user/add-new-user-dialog.tsx b/src/components/user/add-new-user-dialog.tsx new file mode 100644 index 0000000..164716a --- /dev/null +++ b/src/components/user/add-new-user-dialog.tsx @@ -0,0 +1,48 @@ +import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus'; +import { m } from '@/paraglide/messages'; +import { PlusIcon } from '@phosphor-icons/react'; +import { useState } from 'react'; +import AdminCreateUserForm from '../form/admin-create-user-form'; +import { Button } from '../ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../ui/dialog'; + +const AddNewUserButton = () => { + const [_open, _setOpen] = useState(false); + const prevent = usePreventAutoFocus(); + + return ( + + + + + e.preventDefault()} + > + + + + {m.nav_add_new()} + + + {m.nav_add_new()} + + + + + + ); +}; + +export default AddNewUserButton; diff --git a/src/hooks/use-copy-to-clipboard.ts b/src/hooks/use-copy-to-clipboard.ts new file mode 100644 index 0000000..09c4d1f --- /dev/null +++ b/src/hooks/use-copy-to-clipboard.ts @@ -0,0 +1,33 @@ +import { useState } from 'react'; + +export function useCopyToClipboard({ + timeout = 2000, + onCopy, +}: { + timeout?: number; + onCopy?: () => void; +} = {}) { + const [isCopied, setIsCopied] = useState(false); + + const copyToClipboard = (value: string) => { + if (typeof window === 'undefined' || !navigator.clipboard.writeText) { + return; + } + + if (!value) return; + + navigator.clipboard.writeText(value).then(() => { + setIsCopied(true); + + if (onCopy) { + onCopy(); + } + + setTimeout(() => { + setIsCopied(false); + }, timeout); + }, console.error); + }; + + return { isCopied, copyToClipboard }; +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index effdf16..13c5dbb 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -9,6 +9,7 @@ import { } from '@/lib/auth/organization-permissions'; import { ac, admin, user } from '@/lib/auth/permissions'; import { createAuditLog } from '@/service/repository'; +import { DB_TABLE, LOG_ACTION } from '@/types/enum'; import { betterAuth } from 'better-auth'; import { prismaAdapter } from 'better-auth/adapters/prisma'; import { admin as adminPlugin, organization } from 'better-auth/plugins'; @@ -66,6 +67,14 @@ export const auth = betterAuth({ color: '#000000', }, }); + await prisma.setting.create({ + data: { + key: user.id, + value: '{"language": "en"}', + description: '', + relation: 'user', + }, + }); }, }, update: { @@ -79,8 +88,8 @@ export const auth = betterAuth({ ), ); await createAuditLog({ - action: 'update', - tableName: 'user', + action: LOG_ACTION.UPDATE, + tableName: DB_TABLE.USER, recordId: ctx?.context.session?.user.id, oldValue: JSON.stringify(oldUser), newValue: JSON.stringify(newUser), @@ -95,8 +104,8 @@ export const auth = betterAuth({ after: async (account, context) => { if (context?.path === '/change-password') { await createAuditLog({ - action: 'change_password', - tableName: 'account', + action: LOG_ACTION.CHANGE_PASSWORD, + tableName: DB_TABLE.ACCOUNT, recordId: account.id, oldValue: 'Change Password', newValue: 'Change Password', @@ -111,8 +120,8 @@ export const auth = betterAuth({ after: async (session, context) => { if (context?.path.includes('/sign-in')) { await createAuditLog({ - action: 'sign_in', - tableName: 'session', + action: LOG_ACTION.SIGN_IN, + tableName: DB_TABLE.SESSION, recordId: session.id, oldValue: '', newValue: JSON.stringify(session), @@ -125,8 +134,8 @@ export const auth = betterAuth({ after: async (session, context) => { if (context?.path === '/sign-out') { await createAuditLog({ - action: 'sign_out', - tableName: 'session', + action: LOG_ACTION.SIGN_OUT, + tableName: DB_TABLE.SESSION, recordId: session.id, oldValue: JSON.stringify(session), newValue: '', diff --git a/src/routes/(app)/(auth)/kanri/users.tsx b/src/routes/(app)/(auth)/kanri/users.tsx index 91c69c3..e1c5111 100644 --- a/src/routes/(app)/(auth)/kanri/users.tsx +++ b/src/routes/(app)/(auth)/kanri/users.tsx @@ -2,6 +2,7 @@ import DataTable from '@/components/DataTable'; import { Card, CardHeader, CardTitle } from '@/components/ui/card'; import SearchInput from '@/components/ui/search-input'; import { Skeleton } from '@/components/ui/skeleton'; +import AddNewUserButton from '@/components/user/add-new-user-dialog'; import { userColumns } from '@/components/user/user-column'; import useDebounced from '@/hooks/use-debounced'; import { m } from '@/paraglide/messages'; @@ -57,12 +58,13 @@ function RouteComponent() { -
+
+
{data && ( { + .handler(async ({ data, context: { user } }) => { try { const headers = getRequestHeaders(); const result = await auth.api.setUserPassword({ @@ -58,6 +62,16 @@ export const setUserPassword = createServerFn({ method: 'POST' }) }, headers, }); + + await createAuditLog({ + action: LOG_ACTION.CHANGE_PASSWORD, + tableName: DB_TABLE.ACCOUNT, + recordId: data.id, + oldValue: 'Admin Set User Password', + newValue: 'Admin Set User Password', + userId: user.id, + }); + return result; } catch (error) { const { message, code } = parseError(error); @@ -68,8 +82,15 @@ export const setUserPassword = createServerFn({ method: 'POST' }) export const updateUserInformation = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .inputValidator(userUpdateInfoSchema) - .handler(async ({ data }) => { + .handler(async ({ data, context: { user } }) => { try { + const currentUser = await prisma.user.findUnique({ + where: { id: data.id }, + select: { name: true }, + }); + + if (!currentUser) throw Error('User not found'); + const headers = getRequestHeaders(); const result = await auth.api.adminUpdateUser({ body: { @@ -79,6 +100,16 @@ export const updateUserInformation = createServerFn({ method: 'POST' }) // This endpoint requires session cookies. headers, }); + + await createAuditLog({ + action: LOG_ACTION.UPDATE, + tableName: DB_TABLE.USER, + recordId: data.id, + oldValue: JSON.stringify({ name: currentUser.name }), + newValue: JSON.stringify({ name: data.name }), + userId: user.id, + }); + return result; } catch (error) { const { message, code } = parseError(error); @@ -89,8 +120,15 @@ export const updateUserInformation = createServerFn({ method: 'POST' }) export const setUserRole = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .inputValidator(userUpdateRoleSchema) - .handler(async ({ data }) => { + .handler(async ({ data, context: { user } }) => { try { + const currentUser = await prisma.user.findUnique({ + where: { id: data.id }, + select: { role: true }, + }); + + if (!currentUser) throw Error('User not found'); + const headers = getRequestHeaders(); const result = await auth.api.setRole({ body: { @@ -101,6 +139,15 @@ export const setUserRole = createServerFn({ method: 'POST' }) headers, }); + await createAuditLog({ + action: LOG_ACTION.UPDATE, + tableName: DB_TABLE.USER, + recordId: data.id, + oldValue: JSON.stringify({ role: currentUser.role }), + newValue: JSON.stringify({ role: data.role }), + userId: user.id, + }); + return result; } catch (error) { const { message, code } = parseError(error); @@ -111,7 +158,7 @@ export const setUserRole = createServerFn({ method: 'POST' }) export const banUser = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .inputValidator(userBanSchema) - .handler(async ({ data }) => { + .handler(async ({ data, context: { user } }) => { try { const headers = getRequestHeaders(); const result = await auth.api.banUser({ @@ -126,6 +173,16 @@ export const banUser = createServerFn({ method: 'POST' }) // This endpoint requires session cookies. headers, }); + + await createAuditLog({ + action: LOG_ACTION.BAN, + tableName: DB_TABLE.USER, + recordId: data.id, + oldValue: null, + newValue: `Lý do: ${data.banReason} - ${data.banExp} days`, + userId: user.id, + }); + return result; } catch (error) { const { message, code } = parseError(error); @@ -136,7 +193,7 @@ export const banUser = createServerFn({ method: 'POST' }) export const unbanUser = createServerFn({ method: 'POST' }) .middleware([authMiddleware]) .inputValidator(baseUser) - .handler(async ({ data }) => { + .handler(async ({ data, context: { user } }) => { try { const headers = getRequestHeaders(); const result = await auth.api.unbanUser({ @@ -146,9 +203,45 @@ export const unbanUser = createServerFn({ method: 'POST' }) // This endpoint requires session cookies. headers, }); + + await createAuditLog({ + action: LOG_ACTION.UNBAN, + tableName: DB_TABLE.USER, + recordId: data.id, + oldValue: null, + newValue: null, + userId: user.id, + }); + return result; } catch (error) { const { message, code } = parseError(error); throw { message, code }; } }); + +export const createUser = createServerFn({ method: 'POST' }) + .middleware([authMiddleware]) + .inputValidator(userCreateSchema) + .handler(async ({ data, context: { user } }) => { + try { + const result = await auth.api.createUser({ + body: data, + }); + + await createAuditLog({ + action: LOG_ACTION.CREATE, + tableName: DB_TABLE.USER, + recordId: result.user.id, + oldValue: null, + newValue: JSON.stringify(result.user), + userId: user.id, + }); + + return result; + } catch (error) { + console.error(error); + const { message, code } = parseError(error); + throw { message, code }; + } + }); diff --git a/src/service/user.schema.ts b/src/service/user.schema.ts index 928a405..8ddc011 100644 --- a/src/service/user.schema.ts +++ b/src/service/user.schema.ts @@ -29,7 +29,10 @@ export const userUpdateInfoSchema = baseUser.extend({ ), }); -export const RoleEnum = z.enum(['admin', 'user']); +export const RoleEnum = z.enum( + ['admin', 'user'], + m.users_page_message_role_select(), +); export const userUpdateRoleSchema = baseUser.extend({ role: RoleEnum, @@ -43,3 +46,23 @@ export const userBanSchema = baseUser.extend({ ), banExp: z.number().int().min(1, m.users_page_message_select_min_one_day()), }); + +export const userCreateSchema = z.object({ + email: z + .string() + .nonempty(m.common_is_required({ field: m.login_page_form_email() })) + .email(m.login_page_messages_email_invalid()), + password: z + .string() + .min(5, m.users_page_message_user_min()) + .regex(/[A-Z]/, m.users_page_message_contain_uppercase()) + .regex(/[a-z]/, m.users_page_message_contain_lowercase()) + .regex(/[0-9]/, m.users_page_message_contain_number()) + .regex(/[^a-zA-Z0-9]/, m.users_page_message_contain_special()), + name: z.string().nonempty( + m.common_is_required({ + field: m.profile_form_name(), + }), + ), + role: RoleEnum, +}); diff --git a/src/types/enum.ts b/src/types/enum.ts new file mode 100644 index 0000000..59fd043 --- /dev/null +++ b/src/types/enum.ts @@ -0,0 +1,28 @@ +export const LOG_ACTION = { + CHANGE_PASSWORD: 'change_password', + CREATE: 'create', + UPDATE: 'update', + DELETE: 'delete', + SIGN_IN: 'sign_in', + SIGN_OUT: 'sign_out', + BAN: 'ban', + UNBAN: 'unban', +} as const; + +export type LOG_ACTION = (typeof LOG_ACTION)[keyof typeof LOG_ACTION]; + +export const DB_TABLE = { + ACCOUNT: 'account', + AUDIT: 'audit', + INVITATION: 'invitation', + MEMBER: 'member', + ORGANIZATION: 'organization', + SESSION: 'session', + SETTING: 'setting', + USER: 'user', + VERIFICATION: 'verification', + BOX: 'box', + ITEM: 'item', +} as const; + +export type DB_TABLE = (typeof DB_TABLE)[keyof typeof DB_TABLE];