diff --git a/apps/admin/services/admin/index.ts b/apps/admin/services/admin/index.ts index 28e43f1..e70a6dc 100644 --- a/apps/admin/services/admin/index.ts +++ b/apps/admin/services/admin/index.ts @@ -1,5 +1,5 @@ // @ts-ignore - + // API 更新时间: // API 唯一标识: import * as announcement from './announcement'; diff --git a/apps/admin/services/common/index.ts b/apps/admin/services/common/index.ts index 61ba129..73b3bda 100644 --- a/apps/admin/services/common/index.ts +++ b/apps/admin/services/common/index.ts @@ -1,5 +1,5 @@ // @ts-ignore - + // API 更新时间: // API 唯一标识: import * as auth from './auth'; diff --git a/apps/user/app/(main)/(user)/profile/change-password.tsx b/apps/user/app/(main)/(user)/profile/change-password.tsx index ac5b867..86a029a 100644 --- a/apps/user/app/(main)/(user)/profile/change-password.tsx +++ b/apps/user/app/(main)/(user)/profile/change-password.tsx @@ -11,77 +11,67 @@ import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; import { z } from 'zod'; +const FormSchema = z + .object({ + password: z.string().min(6), + repeat_password: z.string(), + }) + .refine((data) => data.password === data.repeat_password, { + message: 'passwordMismatch', + path: ['repeat_password'], + }); + export default function ChangePassword() { const t = useTranslations('profile.accountSettings'); - - const FormSchema = z - .object({ - password: z.string(), - repeat_password: z.string(), - }) - .superRefine(({ password, repeat_password }, ctx) => { - if (password !== repeat_password) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t('passwordMismatch'), - path: ['repeat_password'], - }); - } - }); - const form = useForm>({ resolver: zodResolver(FormSchema), }); async function onSubmit(data: z.infer) { - await updateUserPassword({ - password: data.password, - } as API.UpdateUserPasswordRequest); + await updateUserPassword({ password: data.password }); toast.success(t('updateSuccess')); - form.setValue('password', ''); - form.setValue('repeat_password', ''); + form.reset(); } return ( - - {t('accountSettings')} + + + {t('accountSettings')} + + - -
-
{t('loginPassword')}
-
- - ( - - - - - - - )} - /> - ( - - - - - - - )} - /> - - - -
+ +
+ + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + +
); diff --git a/apps/user/app/(main)/(user)/profile/notify-event.tsx b/apps/user/app/(main)/(user)/profile/notify-event.tsx deleted file mode 100644 index 5276832..0000000 --- a/apps/user/app/(main)/(user)/profile/notify-event.tsx +++ /dev/null @@ -1,137 +0,0 @@ -'use client'; - -import useGlobalStore from '@/config/use-global'; -import { updateUserNotify } from '@/services/user/user'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@workspace/ui/components/form'; -import { Switch } from '@workspace/ui/components/switch'; -import { useTranslations } from 'next-intl'; -import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import { z } from 'zod'; - -const FormSchema = z.object({ - enable_balance_notify: z.boolean(), - enable_login_notify: z.boolean(), - enable_subscribe_notify: z.boolean(), - enable_trade_notify: z.boolean(), -}); - -export default function NotifyEvent() { - const t = useTranslations('profile.notifyEvent'); - const { user, getUserInfo } = useGlobalStore(); - - const form = useForm>({ - resolver: zodResolver(FormSchema), - defaultValues: { - enable_balance_notify: user?.enable_balance_notify, - enable_login_notify: user?.enable_login_notify, - enable_subscribe_notify: user?.enable_subscribe_notify, - enable_trade_notify: user?.enable_trade_notify, - }, - }); - - async function onSubmit(data: z.infer) { - await updateUserNotify(data); - toast.success(t('updateSuccess')); - getUserInfo(); - } - - return ( - - - {t('notificationEvents')} - - -
- - ( - - {t('balanceChange')} - - { - field.onChange(value); - form.handleSubmit(onSubmit)(); - }} - /> - - - - )} - /> - ( - - {t('login')} - - { - field.onChange(value); - form.handleSubmit(onSubmit)(); - }} - /> - - - - )} - /> - ( - - {t('subscribe')} - - { - field.onChange(value); - form.handleSubmit(onSubmit)(); - }} - /> - - - - )} - /> - ( - - {t('finance')} - - { - field.onChange(value); - form.handleSubmit(onSubmit)(); - }} - /> - - - - )} - /> - - -
-
- ); -} diff --git a/apps/user/app/(main)/(user)/profile/notify-settings.tsx b/apps/user/app/(main)/(user)/profile/notify-settings.tsx index 387d742..c9b798f 100644 --- a/apps/user/app/(main)/(user)/profile/notify-settings.tsx +++ b/apps/user/app/(main)/(user)/profile/notify-settings.tsx @@ -1,19 +1,11 @@ 'use client'; import useGlobalStore from '@/config/use-global'; -import { bindTelegram, unbindTelegram, updateUserNotifySetting } from '@/services/user/user'; +import { updateUserNotify } from '@/services/user/user'; import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@workspace/ui/components/button'; import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@workspace/ui/components/form'; -import { Input } from '@workspace/ui/components/input'; +import { Form, FormControl, FormField, FormItem, FormLabel } from '@workspace/ui/components/form'; import { Switch } from '@workspace/ui/components/switch'; import { useTranslations } from 'next-intl'; import { useForm } from 'react-hook-form'; @@ -24,10 +16,14 @@ const FormSchema = z.object({ telegram: z.number().nullish(), enable_email_notify: z.boolean(), enable_telegram_notify: z.boolean(), + enable_balance_notify: z.boolean(), + enable_login_notify: z.boolean(), + enable_subscribe_notify: z.boolean(), + enable_trade_notify: z.boolean(), }); export default function NotifySettings() { - const t = useTranslations('profile.notify'); + const t = useTranslations('profile'); const { user, getUserInfo } = useGlobalStore(); const form = useForm>({ resolver: zodResolver(FormSchema), @@ -35,101 +31,58 @@ export default function NotifySettings() { telegram: user?.telegram, enable_email_notify: user?.enable_email_notify, enable_telegram_notify: user?.enable_telegram_notify, + enable_balance_notify: user?.enable_balance_notify, + enable_login_notify: user?.enable_login_notify, + enable_subscribe_notify: user?.enable_subscribe_notify, + enable_trade_notify: user?.enable_trade_notify, }, }); async function onSubmit(data: z.infer) { - await updateUserNotifySetting(data as API.UpdateUserNotifySettingRequet); - toast.success(t('updateSuccess')); + await updateUserNotify(data); + toast.success(t('notify.updateSuccess')); + getUserInfo(); } return ( - - {t('notificationSettings')} + + + {t('notify.notificationSettings')} + + - +
- - ( - - {t('telegramId')} - -
- { - field.onChange(e.target.value ? Number(e.target.value) : ''); - }} - disabled - /> - -
-
- -
- )} - /> - ( - - {t('emailNotification')} - - { - field.onChange(value); - form.handleSubmit(onSubmit)(); - }} - /> - - - - )} - /> - ( - - {t('telegramNotification')} - - { - field.onChange(value); - form.handleSubmit(onSubmit)(); - }} - /> - - - - )} - /> + +
+ {[ + { name: 'enable_email_notify', label: 'emailNotification' }, + { name: 'enable_telegram_notify', label: 'telegramNotification' }, + { name: 'enable_balance_notify', label: 'balanceChange' }, + { name: 'enable_login_notify', label: 'login' }, + { name: 'enable_subscribe_notify', label: 'subscribe' }, + { name: 'enable_trade_notify', label: 'finance' }, + ].map(({ name, label }) => ( + ( + + + {t(`notify.${label}`)} + + + + + + )} + /> + ))} +
diff --git a/apps/user/app/(main)/(user)/profile/page.tsx b/apps/user/app/(main)/(user)/profile/page.tsx index 6599e97..b8c7280 100644 --- a/apps/user/app/(main)/(user)/profile/page.tsx +++ b/apps/user/app/(main)/(user)/profile/page.tsx @@ -1,12 +1,12 @@ import ChangePassword from './change-password'; -import NotifyEvent from './notify-event'; import NotifySettings from './notify-settings'; +import ThirdPartyAccounts from './third-party-accounts'; export default function Page() { return ( -
+
+ -
); diff --git a/apps/user/app/(main)/(user)/profile/third-party-accounts.tsx b/apps/user/app/(main)/(user)/profile/third-party-accounts.tsx new file mode 100644 index 0000000..545fffc --- /dev/null +++ b/apps/user/app/(main)/(user)/profile/third-party-accounts.tsx @@ -0,0 +1,292 @@ +'use client'; + +import SendCode from '@/app/auth/send-code'; +import useGlobalStore from '@/config/use-global'; +import { bindOAuth } from '@/services/user/user'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button } from '@workspace/ui/components/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@workspace/ui/components/dialog'; +import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/ui/components/form'; +import { Input } from '@workspace/ui/components/input'; +import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select'; +import { Icon } from '@workspace/ui/custom-components/icon'; +import { useTranslations } from 'next-intl'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { z } from 'zod'; + +function MobileBindDialog({ + method, + + onSuccess, + children, +}: { + method?: API.UserAuthMethod; + onSuccess: () => void; + children: React.ReactNode; +}) { + const t = useTranslations('profile.thirdParty'); + const [open, setOpen] = useState(false); + + const formSchema = z.object({ + telephone_area_code: z.string().min(1, 'Area code is required'), + telephone: z.string().min(5, 'Phone number is required'), + telephone_code: z.string().min(4, 'Verification code is required'), + }); + + type MobileBindFormValues = z.infer; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + // @ts-ignore + telephone_area_code: method?.area_code || '1', + telephone: method?.auth_identifier || '', + telephone_code: '', + }, + }); + + const onSubmit = async (values: MobileBindFormValues) => { + try { + toast.success(t('bindSuccess')); + onSuccess(); + setOpen(false); + } catch (error) { + toast.error(t('bindFailed')); + } + }; + + return ( + + {children} + + + {t('bindMobile')} + +
+ + ( + + +
+ ( + + + { + if (value.phone) { + form.setValue('telephone_area_code', value.phone); + } + }} + /> + + + + )} + /> + +
+
+ +
+ )} + /> + + ( + + +
+ + +
+
+ + +
+ )} + /> + + + +
+
+ ); +} + +export default function ThirdPartyAccounts() { + const t = useTranslations('profile.thirdParty'); + const { user, getUserInfo, common } = useGlobalStore(); + const { oauth_methods } = common; + + const accounts = [ + { + id: 'email', + icon: 'logos:mailgun-icon', + name: 'Email', + type: 'Basic', + }, + { + id: 'mobile', + icon: 'mdi:telephone', + name: 'Mobile', + type: 'Basic', + }, + { + id: 'telegram', + icon: 'logos:telegram', + name: 'Telegram', + type: 'OAuth', + }, + { + id: 'apple', + icon: 'uil:apple', + name: 'Apple', + type: 'OAuth', + }, + { + id: 'google', + icon: 'logos:google', + name: 'Google', + type: 'OAuth', + }, + { + id: 'facebook', + icon: 'logos:facebook', + name: 'Facebook', + type: 'OAuth', + }, + { + id: 'github', + icon: 'uil:github', + name: 'GitHub', + type: 'OAuth', + }, + ]; + // .filter((account) => oauth_methods?.includes(account.id)); + + const [editValues, setEditValues] = useState>({}); + + const handleBasicAccountUpdate = async (account: (typeof accounts)[0], value: string) => { + if (account.id === 'email') { + // TODO: Create a new email auth or update the existing one + await getUserInfo(); + toast.success(t('updateSuccess')); + } + }; + + const handleAccountAction = async (account: (typeof accounts)[number]) => { + const isBound = user?.auth_methods?.find( + (auth) => auth.auth_type === account.id, + )?.auth_identifier; + if (isBound) { + // unbindOAuth + // await unbindOAuth(account.id); + await getUserInfo(); + } else { + const res = await bindOAuth({ + method: account.id, + redirect: `${window.location.origin}/bind/${account.id}`, + }); + if (res.data?.data?.url) { + window.location.href = res.data.data.url; + } + } + }; + + return ( + <> + + + {t('title')} + + +
+ {accounts.map((account) => { + const method = user?.auth_methods?.find((auth) => auth.auth_type === account.id); + const isEditing = account.id === 'email'; + const currentValue = method?.auth_identifier || editValues[account.id]; + const displayValue = isEditing + ? currentValue + : method?.auth_identifier || t(`${account.id}.description`); + + return ( +
+ + + {account.name} + +
+ + isEditing && + setEditValues((prev) => ({ ...prev, [account.id]: e.target.value })) + } + onKeyDown={(e) => { + if (e.key === 'Enter' && isEditing) { + handleBasicAccountUpdate(account, currentValue); + } + }} + /> + {account.id === 'mobile' ? ( + + + + ) : ( + + )} +
+
+ ); + })} +
+
+
+ + ); +} diff --git a/apps/user/app/auth/page.tsx b/apps/user/app/auth/page.tsx index c641551..d1a314c 100644 --- a/apps/user/app/auth/page.tsx +++ b/apps/user/app/auth/page.tsx @@ -28,27 +28,21 @@ export default function Page() { const { common } = useGlobalStore(); const { site, auth, oauth_methods } = common; - const AUTH_COMPONENT_MAP = { - email: , - sms: , - } as const; + const AUTH_METHODS = [ + { + key: 'email', + enabled: auth.email.enable, + children: , + }, + { + key: 'mobile', + enabled: auth.mobile.enable, + children: , + }, + ].filter((method) => method.enabled); - type AuthMethod = keyof typeof AUTH_COMPONENT_MAP; + const OAUTH_METHODS = oauth_methods?.filter((method) => !['mobile', 'email'].includes(method)); - const enabledAuthMethods = (Object.keys(AUTH_COMPONENT_MAP) as AuthMethod[]).filter((key) => { - const value = auth[key]; - const enabledKey = `${key}_enabled` as const; - - if (typeof value !== 'object' || value === null) { - return false; - } - - if (!(enabledKey in value)) { - return false; - } - const isEnabled = (value as unknown as Record)[enabledKey]; - return isEnabled; - }); return (
@@ -79,27 +73,27 @@ export default function Page() {
{t('verifyAccountDesc')}
- {enabledAuthMethods.length === 1 - ? AUTH_COMPONENT_MAP[enabledAuthMethods[0] as AuthMethod] - : enabledAuthMethods[0] && ( - + {AUTH_METHODS.length === 1 + ? AUTH_METHODS[0]?.children + : AUTH_METHODS[0] && ( + - {enabledAuthMethods.map((method) => ( - - {t(`methods.${method}`)} + {AUTH_METHODS.map((item) => ( + + {t(`methods.${item.key}`)} ))} - {enabledAuthMethods.map((method) => ( - - {AUTH_COMPONENT_MAP[method]} + {AUTH_METHODS.map((item) => ( + + {item.children} ))} )}
- {oauth_methods?.length > 0 && ( + {OAUTH_METHODS?.length > 0 && ( <>
@@ -107,7 +101,7 @@ export default function Page() {
- {oauth_methods?.map((method: any) => { + {OAUTH_METHODS?.map((method: any) => { return (