mirror of
https://github.com/coaidev/coai.git
synced 2025-05-21 05:50:14 +09:00
feat: support unforced smtp registration (#42)
This commit is contained in:
parent
0bbc2d9ee2
commit
06ccf4e25c
@ -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<SiteInfo> {
|
||||
@ -30,6 +32,7 @@ export async function getSiteInfo(): Promise<SiteInfo> {
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -320,3 +320,7 @@ input[type="number"] {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: hsl(var(--text-secondary)) !important;
|
||||
}
|
||||
|
@ -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 (
|
||||
<div className={`paragraph-description`}>
|
||||
<div
|
||||
className={cn(
|
||||
"paragraph-description",
|
||||
border && `px-4 py-4 border rounded-lg`,
|
||||
)}
|
||||
>
|
||||
<Info size={16} />
|
||||
<Markdown children={children} />
|
||||
</div>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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": "搜索域名后缀",
|
||||
|
@ -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}}",
|
||||
|
@ -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}}のためにリクエストできませんでした",
|
||||
|
@ -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}}",
|
||||
|
@ -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<ResetForm>(), {
|
||||
email: "",
|
||||
code: "",
|
||||
@ -63,6 +69,12 @@ function Forgot() {
|
||||
<Card className={`auth-card`}>
|
||||
<CardContent className={`pb-0`}>
|
||||
<div className={`auth-wrapper`}>
|
||||
{!enabled && (
|
||||
<Alert className={`p-4`}>
|
||||
<AlertCircle className={`h-4 w-4`} />
|
||||
<AlertDescription>{t("auth.disabled-mail")}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<Label>
|
||||
<Require />
|
||||
{t("auth.email")}
|
||||
@ -99,6 +111,7 @@ function Forgot() {
|
||||
loading={true}
|
||||
onClick={onVerify}
|
||||
tick={60}
|
||||
disabled={!enabled}
|
||||
>
|
||||
{t("auth.send-code")}
|
||||
</TickButton>
|
||||
@ -144,7 +157,12 @@ function Forgot() {
|
||||
}
|
||||
/>
|
||||
|
||||
<Button onClick={onSubmit} className={`mt-2`} loading={true}>
|
||||
<Button
|
||||
disabled={!enabled}
|
||||
onClick={onSubmit}
|
||||
className={`mt-2`}
|
||||
loading={true}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -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) {
|
||||
|
||||
<div className={`flex flex-row`}>
|
||||
<Input
|
||||
placeholder={t("auth.code-placeholder")}
|
||||
disabled={!mail}
|
||||
placeholder={
|
||||
mail
|
||||
? t("auth.code-placeholder")
|
||||
: t("auth.code-disabled-placeholder")
|
||||
}
|
||||
value={form.code}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
@ -191,6 +200,7 @@ function Verify({ form, dispatch, setNext }: CompProps) {
|
||||
loading={true}
|
||||
onClick={onVerify}
|
||||
tick={60}
|
||||
disabled={!mail}
|
||||
>
|
||||
{t("auth.send-code")}
|
||||
</TickButton>
|
||||
|
@ -238,6 +238,19 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
|
||||
const [mailDialog, setMailDialog] = useState<boolean>(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<MailState>) {
|
||||
configParagraph={true}
|
||||
isCollapsed={true}
|
||||
>
|
||||
{!valid && (
|
||||
<ParagraphDescription border={true}>
|
||||
{t("admin.system.mailConfNotValid")}
|
||||
</ParagraphDescription>
|
||||
)}
|
||||
<ParagraphItem>
|
||||
<Label>
|
||||
<Require /> {t("admin.system.mailHost")}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import infoReducer from "./info";
|
||||
import globalReducer from "./globals";
|
||||
import menuReducer from "./menu";
|
||||
import authReducer from "./auth";
|
||||
@ -13,6 +14,7 @@ import settingsReducer from "./settings";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
info: infoReducer,
|
||||
global: globalReducer,
|
||||
menu: menuReducer,
|
||||
auth: authReducer,
|
||||
|
23
app/src/store/info.ts
Normal file
23
app/src/store/info.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { InfoForm } from "@/events/info.ts";
|
||||
import { RootState } from "@/store/index.ts";
|
||||
|
||||
export const infoSlice = createSlice({
|
||||
name: "info",
|
||||
initialState: {
|
||||
mail: false,
|
||||
} as InfoForm,
|
||||
reducers: {
|
||||
setForm: (state, action) => {
|
||||
const form = action.payload as InfoForm;
|
||||
state.mail = form.mail ?? false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setForm } = infoSlice.actions;
|
||||
|
||||
export default infoSlice.reducer;
|
||||
|
||||
export const infoDataSelector = (state: RootState): InfoForm => state.info;
|
||||
export const infoMailSelector = (state: RootState): boolean => state.info.mail;
|
@ -106,11 +106,13 @@ func SignUp(c *gin.Context, form RegisterForm) (string, error) {
|
||||
email := strings.TrimSpace(form.Email)
|
||||
code := strings.TrimSpace(form.Code)
|
||||
|
||||
enableVerify := channel.SystemInstance.IsMailValid()
|
||||
|
||||
if !utils.All(
|
||||
validateUsername(username),
|
||||
validatePassword(password),
|
||||
validateEmail(email),
|
||||
validateCode(code),
|
||||
!enableVerify || validateCode(code),
|
||||
) {
|
||||
return "", errors.New("invalid username/password/email format")
|
||||
}
|
||||
@ -127,7 +129,7 @@ func SignUp(c *gin.Context, form RegisterForm) (string, error) {
|
||||
return "", fmt.Errorf("email is already taken, please try another one email (your current email: %s)", email)
|
||||
}
|
||||
|
||||
if !checkCode(c, cache, email, code) {
|
||||
if enableVerify && !checkCode(c, cache, email, code) {
|
||||
return "", errors.New("invalid email verification code")
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ type RegisterForm struct {
|
||||
Username string `form:"username" binding:"required"`
|
||||
Password string `form:"password" binding:"required"`
|
||||
Email string `form:"email" binding:"required"`
|
||||
Code string `form:"code" binding:"required"`
|
||||
Code string `form:"code"`
|
||||
}
|
||||
|
||||
type VerifyForm struct {
|
||||
|
@ -15,6 +15,7 @@ type ApiInfo struct {
|
||||
Docs string `json:"docs"`
|
||||
Announcement string `json:"announcement"`
|
||||
BuyLink string `json:"buy_link"`
|
||||
Mail bool `json:"mail"`
|
||||
}
|
||||
|
||||
type generalState struct {
|
||||
@ -87,6 +88,7 @@ func (c *SystemConfig) AsInfo() ApiInfo {
|
||||
Docs: c.General.Docs,
|
||||
Announcement: c.Site.Announcement,
|
||||
BuyLink: c.Site.BuyLink,
|
||||
Mail: c.IsMailValid(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ func (l *Limiter) RateLimit(client *redis.Client, ip string, path string) bool {
|
||||
|
||||
var limits = map[string]Limiter{
|
||||
"/login": {Duration: 10, Count: 20},
|
||||
"/register": {Duration: 120, Count: 10},
|
||||
"/register": {Duration: 600, Count: 5},
|
||||
"/verify": {Duration: 120, Count: 5},
|
||||
"/reset": {Duration: 120, Count: 10},
|
||||
"/apikey": {Duration: 1, Count: 2},
|
||||
|
Loading…
Reference in New Issue
Block a user