diff --git a/app/src/admin/api/info.ts b/app/src/admin/api/info.ts index d3d49cd..8d0e22a 100644 --- a/app/src/admin/api/info.ts +++ b/app/src/admin/api/info.ts @@ -7,6 +7,7 @@ import { setBuyLink, setDocsUrl, } from "@/conf/env.ts"; +import { infoEvent } from "@/events/info.ts"; export type SiteInfo = { title: string; @@ -15,6 +16,7 @@ export type SiteInfo = { file: string; announcement: string; buy_link: string; + mail: boolean; }; export async function getSiteInfo(): Promise { @@ -30,6 +32,7 @@ export async function getSiteInfo(): Promise { file: "", announcement: "", buy_link: "", + mail: false, }; } } @@ -42,5 +45,9 @@ export function syncSiteInfo() { setBlobEndpoint(info.file); setAnnouncement(info.announcement); setBuyLink(info.buy_link); + + infoEvent.emit({ + mail: info.mail, + }); }); } diff --git a/app/src/assets/ui.less b/app/src/assets/ui.less index 6663ad5..6b6d04e 100644 --- a/app/src/assets/ui.less +++ b/app/src/assets/ui.less @@ -320,3 +320,7 @@ input[type="number"] { transform: translateY(-1px); } } + +.text-secondary { + color: hsl(var(--text-secondary)) !important; +} diff --git a/app/src/components/Paragraph.tsx b/app/src/components/Paragraph.tsx index 32c0bb7..e7ef599 100644 --- a/app/src/components/Paragraph.tsx +++ b/app/src/components/Paragraph.tsx @@ -84,9 +84,21 @@ function ParagraphItem({ ); } -export function ParagraphDescription({ children }: { children: string }) { +type ParagraphDescriptionProps = { + children: string; + border?: boolean; +}; +export function ParagraphDescription({ + children, + border, +}: ParagraphDescriptionProps) { return ( -
+
diff --git a/app/src/components/app/AppProvider.tsx b/app/src/components/app/AppProvider.tsx index 5ab284c..2c504b4 100644 --- a/app/src/components/app/AppProvider.tsx +++ b/app/src/components/app/AppProvider.tsx @@ -19,10 +19,17 @@ import { Model } from "@/api/types.ts"; import { ChargeProps, nonBilling } from "@/admin/charge.ts"; import { dispatchSubscriptionData } from "@/store/globals.ts"; import { marketEvent } from "@/events/market.ts"; +import { useEffect } from "react"; +import { infoEvent } from "@/events/info.ts"; +import { setForm } from "@/store/info.ts"; function AppProvider() { const dispatch = useDispatch(); + useEffect(() => { + infoEvent.bind((data) => dispatch(setForm(data))); + }, []); + useEffectAsync(async () => { marketEvent.emit(false); diff --git a/app/src/resources/i18n/cn.json b/app/src/resources/i18n/cn.json index dc03c97..ce7d2c5 100644 --- a/app/src/resources/i18n/cn.json +++ b/app/src/resources/i18n/cn.json @@ -58,6 +58,7 @@ "username-or-email-placeholder": "请输入用户名或邮箱", "code": "验证码", "code-placeholder": "请输入验证码", + "code-disabled-placeholder": "无需进行邮箱验证", "send-code": "发送", "incorrect-info": "填错信息?", "fall-back": "回退一步", @@ -79,7 +80,8 @@ "send-code-failed": "发送失败", "send-code-failed-prompt": "验证码发送失败,原因:{{reason}}", "register-success": "注册成功", - "register-success-prompt": "您已成功注册,欢迎你的到来!" + "register-success-prompt": "您已成功注册,欢迎你的到来!", + "disabled-mail": "当前站点的邮箱已被禁用,请联系管理员开启发件功能。" }, "tag": { "free": "免费", @@ -588,6 +590,7 @@ "mailPass": "密码", "mailFrom": "发件人", "mailEnableWhitelist": "启用域名后缀白名单", + "mailConfNotValid": "SMTP 发件参数未正确配置,已禁用邮箱验证", "mailWhitelist": "域名后缀白名单", "mailWhitelistSelected": "已选 {{length}} 个域名邮箱", "mailWhitelistSearchPlaceholder": "搜索域名后缀", diff --git a/app/src/resources/i18n/en.json b/app/src/resources/i18n/en.json index 1176c76..22289d8 100644 --- a/app/src/resources/i18n/en.json +++ b/app/src/resources/i18n/en.json @@ -475,7 +475,8 @@ "mailWhitelistSearchPlaceholder": "Search Domain Suffixes", "customWhitelistPlaceholder": "Please enter a list of custom domain suffixes (which will appear in the list of options to choose from), separated by commas, e.g.: example.com, example.net", "buyLink": "Buy Link", - "buyLinkPlaceholder": "Please enter the card secret purchase link, leave blank to not show the purchase button" + "buyLinkPlaceholder": "Please enter the card secret purchase link, leave blank to not show the purchase button", + "mailConfNotValid": "SMTP send parameters are not configured correctly, mailbox verification is disabled" }, "user": "User Management", "invitation-code": "Invitation Code", @@ -601,7 +602,9 @@ "send-code-failed": "Send failed", "send-code-failed-prompt": "Failed to send verification code, reason: {{reason}}", "register-success": "Account created !", - "register-success-prompt": "You have successfully registered, welcome!" + "register-success-prompt": "You have successfully registered, welcome!", + "disabled-mail": "The mailbox of the current site has been disabled, please contact the administrator to enable the mailing function.", + "code-disabled-placeholder": "No email verification required" }, "reset": "Reset", "request-error": "Request failed for {{reason}}", diff --git a/app/src/resources/i18n/ja.json b/app/src/resources/i18n/ja.json index 8e68111..0b0e0ff 100644 --- a/app/src/resources/i18n/ja.json +++ b/app/src/resources/i18n/ja.json @@ -475,7 +475,8 @@ "mailWhitelistSearchPlaceholder": "ドメイン接尾辞を検索", "customWhitelistPlaceholder": "カスタムドメインサフィックスのリスト(選択するオプションのリストに表示されます)をカンマで区切って入力してください。例: example.com、example.net", "buyLink": "購入リンク", - "buyLinkPlaceholder": "カードシークレット購入リンクを入力してください。購入ボタンを表示しない場合は空白のままにしてください" + "buyLinkPlaceholder": "カードシークレット購入リンクを入力してください。購入ボタンを表示しない場合は空白のままにしてください", + "mailConfNotValid": "SMTP送信パラメータが正しく設定されていません。メールボックスの検証が無効になっています" }, "user": "ユーザー管理", "invitation-code": "招待コード", @@ -601,7 +602,9 @@ "send-code-failed": "送信失敗", "send-code-failed-prompt": "認証コードの送信に失敗しました。理由:{{ reason}}", "register-success": "登録に成功しました", - "register-success-prompt": "登録が完了しました。ようこそ!" + "register-success-prompt": "登録が完了しました。ようこそ!", + "disabled-mail": "現在のサイトのメールボックスは無効になっています。管理者に連絡して郵送機能を有効にしてください。", + "code-disabled-placeholder": "メールアドレスの認証は必要ありません" }, "reset": "リセット", "request-error": "{{reason}}のためにリクエストできませんでした", diff --git a/app/src/resources/i18n/ru.json b/app/src/resources/i18n/ru.json index 67361c1..6cc8c53 100644 --- a/app/src/resources/i18n/ru.json +++ b/app/src/resources/i18n/ru.json @@ -475,7 +475,8 @@ "mailWhitelistSearchPlaceholder": "Поиск суффиксов доменов", "customWhitelistPlaceholder": "Введите список пользовательских суффиксов домена (которые появятся в списке опций на выбор), разделенных запятыми, например: example.com, example.net", "buyLink": "Ссылка на покупку", - "buyLinkPlaceholder": "Введите ссылку на секретную покупку карты, оставьте поле пустым, чтобы не показывать кнопку покупки" + "buyLinkPlaceholder": "Введите ссылку на секретную покупку карты, оставьте поле пустым, чтобы не показывать кнопку покупки", + "mailConfNotValid": "Параметры отправки SMTP настроены неправильно, проверка почтового ящика отключена" }, "user": "Управление пользователями", "invitation-code": "Код приглашения", @@ -601,7 +602,9 @@ "send-code-failed": "Не удалось отправить", "send-code-failed-prompt": "Не удалось отправить код подтверждения, причина: {{reason}}", "register-success": "Регистрация прошла успешно", - "register-success-prompt": "Вы успешно зарегистрировались, добро пожаловать!" + "register-success-prompt": "Вы успешно зарегистрировались, добро пожаловать!", + "disabled-mail": "Почтовый ящик текущего сайта отключен. Чтобы включить функцию рассылки, обратитесь к администратору.", + "code-disabled-placeholder": "Подтверждение адреса электронной почты не требуется" }, "reset": "сброс", "request-error": "Запрос не выполнен по {{reason}}", diff --git a/app/src/routes/Forgot.tsx b/app/src/routes/Forgot.tsx index 927e9da..4dc17b3 100644 --- a/app/src/routes/Forgot.tsx +++ b/app/src/routes/Forgot.tsx @@ -11,14 +11,20 @@ import Require, { LengthRangeRequired, SameRequired, } from "@/components/Require.tsx"; +import { Alert, AlertDescription } from "@/components/ui/alert"; import { Input } from "@/components/ui/input.tsx"; import { Button } from "@/components/ui/button.tsx"; import TickButton from "@/components/TickButton.tsx"; import { appLogo } from "@/conf/env.ts"; +import { useSelector } from "react-redux"; +import { infoMailSelector } from "@/store/info.ts"; +import { AlertCircle } from "lucide-react"; function Forgot() { const { t } = useTranslation(); const { toast } = useToast(); + const enabled = useSelector(infoMailSelector); + const [form, dispatch] = useReducer(formReducer(), { email: "", code: "", @@ -63,6 +69,12 @@ function Forgot() {
+ {!enabled && ( + + + {t("auth.disabled-mail")} + + )}
diff --git a/app/src/routes/Register.tsx b/app/src/routes/Register.tsx index 541b260..58a7029 100644 --- a/app/src/routes/Register.tsx +++ b/app/src/routes/Register.tsx @@ -15,8 +15,9 @@ import { doRegister, RegisterForm, sendCode } from "@/api/auth.ts"; import { useToast } from "@/components/ui/use-toast.ts"; import TickButton from "@/components/TickButton.tsx"; import { validateToken } from "@/store/auth.ts"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { appLogo, appName } from "@/conf/env.ts"; +import { infoMailSelector } from "@/store/info.ts"; type CompProps = { form: RegisterForm; @@ -128,10 +129,13 @@ function Verify({ form, dispatch, setNext }: CompProps) { const { toast } = useToast(); const globalDispatch = useDispatch(); + const mail = useSelector(infoMailSelector); + const onSubmit = async () => { const data = doFormat(form); - if (!isEmailValid(data.email) || !data.code.trim().length) return; + if (!isEmailValid(data.email)) return; + if (mail && data.code.trim().length === 0) return; const resp = await doRegister(data); if (!resp.status) { @@ -177,7 +181,12 @@ function Verify({ form, dispatch, setNext }: CompProps) {
dispatch({ @@ -191,6 +200,7 @@ function Verify({ form, dispatch, setNext }: CompProps) { loading={true} onClick={onVerify} tick={60} + disabled={!mail} > {t("auth.send-code")} diff --git a/app/src/routes/admin/System.tsx b/app/src/routes/admin/System.tsx index 7c4363b..c25c3da 100644 --- a/app/src/routes/admin/System.tsx +++ b/app/src/routes/admin/System.tsx @@ -238,6 +238,19 @@ function Mail({ data, dispatch, onChange }: CompProps) { const [mailDialog, setMailDialog] = useState(false); + const valid = useMemo((): boolean => { + return ( + data.host.length > 0 && + data.port > 0 && + data.port < 65535 && + data.username.length > 0 && + data.password.length > 0 && + data.from.length > 0 && + /\w+@\w+\.\w+/.test(data.from) && + !data.username.includes("@") + ); + }, [data]); + const onTest = async () => { if (!email.trim()) return; await onChange(false); @@ -262,6 +275,11 @@ function Mail({ data, dispatch, onChange }: CompProps) { configParagraph={true} isCollapsed={true} > + {!valid && ( + + {t("admin.system.mailConfNotValid")} + + )}