Skip to content

Commit 06ccf4e

Browse files
committed
feat: support unforced smtp registration (#42)
1 parent 0bbc2d9 commit 06ccf4e

File tree

17 files changed

+134
-17
lines changed

17 files changed

+134
-17
lines changed

app/src/admin/api/info.ts

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
setBuyLink,
88
setDocsUrl,
99
} from "@/conf/env.ts";
10+
import { infoEvent } from "@/events/info.ts";
1011

1112
export type SiteInfo = {
1213
title: string;
@@ -15,6 +16,7 @@ export type SiteInfo = {
1516
file: string;
1617
announcement: string;
1718
buy_link: string;
19+
mail: boolean;
1820
};
1921

2022
export async function getSiteInfo(): Promise<SiteInfo> {
@@ -30,6 +32,7 @@ export async function getSiteInfo(): Promise<SiteInfo> {
3032
file: "",
3133
announcement: "",
3234
buy_link: "",
35+
mail: false,
3336
};
3437
}
3538
}
@@ -42,5 +45,9 @@ export function syncSiteInfo() {
4245
setBlobEndpoint(info.file);
4346
setAnnouncement(info.announcement);
4447
setBuyLink(info.buy_link);
48+
49+
infoEvent.emit({
50+
mail: info.mail,
51+
});
4552
});
4653
}

app/src/assets/ui.less

+4
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,7 @@ input[type="number"] {
320320
transform: translateY(-1px);
321321
}
322322
}
323+
324+
.text-secondary {
325+
color: hsl(var(--text-secondary)) !important;
326+
}

app/src/components/Paragraph.tsx

+14-2
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,21 @@ function ParagraphItem({
8484
);
8585
}
8686

87-
export function ParagraphDescription({ children }: { children: string }) {
87+
type ParagraphDescriptionProps = {
88+
children: string;
89+
border?: boolean;
90+
};
91+
export function ParagraphDescription({
92+
children,
93+
border,
94+
}: ParagraphDescriptionProps) {
8895
return (
89-
<div className={`paragraph-description`}>
96+
<div
97+
className={cn(
98+
"paragraph-description",
99+
border && `px-4 py-4 border rounded-lg`,
100+
)}
101+
>
90102
<Info size={16} />
91103
<Markdown children={children} />
92104
</div>

app/src/components/app/AppProvider.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,17 @@ import { Model } from "@/api/types.ts";
1919
import { ChargeProps, nonBilling } from "@/admin/charge.ts";
2020
import { dispatchSubscriptionData } from "@/store/globals.ts";
2121
import { marketEvent } from "@/events/market.ts";
22+
import { useEffect } from "react";
23+
import { infoEvent } from "@/events/info.ts";
24+
import { setForm } from "@/store/info.ts";
2225

2326
function AppProvider() {
2427
const dispatch = useDispatch();
2528

29+
useEffect(() => {
30+
infoEvent.bind((data) => dispatch(setForm(data)));
31+
}, []);
32+
2633
useEffectAsync(async () => {
2734
marketEvent.emit(false);
2835

app/src/resources/i18n/cn.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"username-or-email-placeholder": "请输入用户名或邮箱",
5959
"code": "验证码",
6060
"code-placeholder": "请输入验证码",
61+
"code-disabled-placeholder": "无需进行邮箱验证",
6162
"send-code": "发送",
6263
"incorrect-info": "填错信息?",
6364
"fall-back": "回退一步",
@@ -79,7 +80,8 @@
7980
"send-code-failed": "发送失败",
8081
"send-code-failed-prompt": "验证码发送失败,原因:{{reason}}",
8182
"register-success": "注册成功",
82-
"register-success-prompt": "您已成功注册,欢迎你的到来!"
83+
"register-success-prompt": "您已成功注册,欢迎你的到来!",
84+
"disabled-mail": "当前站点的邮箱已被禁用,请联系管理员开启发件功能。"
8385
},
8486
"tag": {
8587
"free": "免费",
@@ -588,6 +590,7 @@
588590
"mailPass": "密码",
589591
"mailFrom": "发件人",
590592
"mailEnableWhitelist": "启用域名后缀白名单",
593+
"mailConfNotValid": "SMTP 发件参数未正确配置,已禁用邮箱验证",
591594
"mailWhitelist": "域名后缀白名单",
592595
"mailWhitelistSelected": "已选 {{length}} 个域名邮箱",
593596
"mailWhitelistSearchPlaceholder": "搜索域名后缀",

app/src/resources/i18n/en.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,8 @@
475475
"mailWhitelistSearchPlaceholder": "Search Domain Suffixes",
476476
"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",
477477
"buyLink": "Buy Link",
478-
"buyLinkPlaceholder": "Please enter the card secret purchase link, leave blank to not show the purchase button"
478+
"buyLinkPlaceholder": "Please enter the card secret purchase link, leave blank to not show the purchase button",
479+
"mailConfNotValid": "SMTP send parameters are not configured correctly, mailbox verification is disabled"
479480
},
480481
"user": "User Management",
481482
"invitation-code": "Invitation Code",
@@ -601,7 +602,9 @@
601602
"send-code-failed": "Send failed",
602603
"send-code-failed-prompt": "Failed to send verification code, reason: {{reason}}",
603604
"register-success": "Account created !",
604-
"register-success-prompt": "You have successfully registered, welcome!"
605+
"register-success-prompt": "You have successfully registered, welcome!",
606+
"disabled-mail": "The mailbox of the current site has been disabled, please contact the administrator to enable the mailing function.",
607+
"code-disabled-placeholder": "No email verification required"
605608
},
606609
"reset": "Reset",
607610
"request-error": "Request failed for {{reason}}",

app/src/resources/i18n/ja.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,8 @@
475475
"mailWhitelistSearchPlaceholder": "ドメイン接尾辞を検索",
476476
"customWhitelistPlaceholder": "カスタムドメインサフィックスのリスト(選択するオプションのリストに表示されます)をカンマで区切って入力してください。例: example.com、example.net",
477477
"buyLink": "購入リンク",
478-
"buyLinkPlaceholder": "カードシークレット購入リンクを入力してください。購入ボタンを表示しない場合は空白のままにしてください"
478+
"buyLinkPlaceholder": "カードシークレット購入リンクを入力してください。購入ボタンを表示しない場合は空白のままにしてください",
479+
"mailConfNotValid": "SMTP送信パラメータが正しく設定されていません。メールボックスの検証が無効になっています"
479480
},
480481
"user": "ユーザー管理",
481482
"invitation-code": "招待コード",
@@ -601,7 +602,9 @@
601602
"send-code-failed": "送信失敗",
602603
"send-code-failed-prompt": "認証コードの送信に失敗しました。理由:{{ reason}}",
603604
"register-success": "登録に成功しました",
604-
"register-success-prompt": "登録が完了しました。ようこそ!"
605+
"register-success-prompt": "登録が完了しました。ようこそ!",
606+
"disabled-mail": "現在のサイトのメールボックスは無効になっています。管理者に連絡して郵送機能を有効にしてください。",
607+
"code-disabled-placeholder": "メールアドレスの認証は必要ありません"
605608
},
606609
"reset": "リセット",
607610
"request-error": "{{reason}}のためにリクエストできませんでした",

app/src/resources/i18n/ru.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,8 @@
475475
"mailWhitelistSearchPlaceholder": "Поиск суффиксов доменов",
476476
"customWhitelistPlaceholder": "Введите список пользовательских суффиксов домена (которые появятся в списке опций на выбор), разделенных запятыми, например: example.com, example.net",
477477
"buyLink": "Ссылка на покупку",
478-
"buyLinkPlaceholder": "Введите ссылку на секретную покупку карты, оставьте поле пустым, чтобы не показывать кнопку покупки"
478+
"buyLinkPlaceholder": "Введите ссылку на секретную покупку карты, оставьте поле пустым, чтобы не показывать кнопку покупки",
479+
"mailConfNotValid": "Параметры отправки SMTP настроены неправильно, проверка почтового ящика отключена"
479480
},
480481
"user": "Управление пользователями",
481482
"invitation-code": "Код приглашения",
@@ -601,7 +602,9 @@
601602
"send-code-failed": "Не удалось отправить",
602603
"send-code-failed-prompt": "Не удалось отправить код подтверждения, причина: {{reason}}",
603604
"register-success": "Регистрация прошла успешно",
604-
"register-success-prompt": "Вы успешно зарегистрировались, добро пожаловать!"
605+
"register-success-prompt": "Вы успешно зарегистрировались, добро пожаловать!",
606+
"disabled-mail": "Почтовый ящик текущего сайта отключен. Чтобы включить функцию рассылки, обратитесь к администратору.",
607+
"code-disabled-placeholder": "Подтверждение адреса электронной почты не требуется"
605608
},
606609
"reset": "сброс",
607610
"request-error": "Запрос не выполнен по {{reason}}",

app/src/routes/Forgot.tsx

+19-1
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@ import Require, {
1111
LengthRangeRequired,
1212
SameRequired,
1313
} from "@/components/Require.tsx";
14+
import { Alert, AlertDescription } from "@/components/ui/alert";
1415
import { Input } from "@/components/ui/input.tsx";
1516
import { Button } from "@/components/ui/button.tsx";
1617
import TickButton from "@/components/TickButton.tsx";
1718
import { appLogo } from "@/conf/env.ts";
19+
import { useSelector } from "react-redux";
20+
import { infoMailSelector } from "@/store/info.ts";
21+
import { AlertCircle } from "lucide-react";
1822

1923
function Forgot() {
2024
const { t } = useTranslation();
2125
const { toast } = useToast();
26+
const enabled = useSelector(infoMailSelector);
27+
2228
const [form, dispatch] = useReducer(formReducer<ResetForm>(), {
2329
email: "",
2430
code: "",
@@ -63,6 +69,12 @@ function Forgot() {
6369
<Card className={`auth-card`}>
6470
<CardContent className={`pb-0`}>
6571
<div className={`auth-wrapper`}>
72+
{!enabled && (
73+
<Alert className={`p-4`}>
74+
<AlertCircle className={`h-4 w-4`} />
75+
<AlertDescription>{t("auth.disabled-mail")}</AlertDescription>
76+
</Alert>
77+
)}
6678
<Label>
6779
<Require />
6880
{t("auth.email")}
@@ -99,6 +111,7 @@ function Forgot() {
99111
loading={true}
100112
onClick={onVerify}
101113
tick={60}
114+
disabled={!enabled}
102115
>
103116
{t("auth.send-code")}
104117
</TickButton>
@@ -144,7 +157,12 @@ function Forgot() {
144157
}
145158
/>
146159

147-
<Button onClick={onSubmit} className={`mt-2`} loading={true}>
160+
<Button
161+
disabled={!enabled}
162+
onClick={onSubmit}
163+
className={`mt-2`}
164+
loading={true}
165+
>
148166
{t("reset")}
149167
</Button>
150168
</div>

app/src/routes/Register.tsx

+13-3
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import { doRegister, RegisterForm, sendCode } from "@/api/auth.ts";
1515
import { useToast } from "@/components/ui/use-toast.ts";
1616
import TickButton from "@/components/TickButton.tsx";
1717
import { validateToken } from "@/store/auth.ts";
18-
import { useDispatch } from "react-redux";
18+
import { useDispatch, useSelector } from "react-redux";
1919
import { appLogo, appName } from "@/conf/env.ts";
20+
import { infoMailSelector } from "@/store/info.ts";
2021

2122
type CompProps = {
2223
form: RegisterForm;
@@ -128,10 +129,13 @@ function Verify({ form, dispatch, setNext }: CompProps) {
128129
const { toast } = useToast();
129130
const globalDispatch = useDispatch();
130131

132+
const mail = useSelector(infoMailSelector);
133+
131134
const onSubmit = async () => {
132135
const data = doFormat(form);
133136

134-
if (!isEmailValid(data.email) || !data.code.trim().length) return;
137+
if (!isEmailValid(data.email)) return;
138+
if (mail && data.code.trim().length === 0) return;
135139

136140
const resp = await doRegister(data);
137141
if (!resp.status) {
@@ -177,7 +181,12 @@ function Verify({ form, dispatch, setNext }: CompProps) {
177181

178182
<div className={`flex flex-row`}>
179183
<Input
180-
placeholder={t("auth.code-placeholder")}
184+
disabled={!mail}
185+
placeholder={
186+
mail
187+
? t("auth.code-placeholder")
188+
: t("auth.code-disabled-placeholder")
189+
}
181190
value={form.code}
182191
onChange={(e) =>
183192
dispatch({
@@ -191,6 +200,7 @@ function Verify({ form, dispatch, setNext }: CompProps) {
191200
loading={true}
192201
onClick={onVerify}
193202
tick={60}
203+
disabled={!mail}
194204
>
195205
{t("auth.send-code")}
196206
</TickButton>

app/src/routes/admin/System.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,19 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
238238

239239
const [mailDialog, setMailDialog] = useState<boolean>(false);
240240

241+
const valid = useMemo((): boolean => {
242+
return (
243+
data.host.length > 0 &&
244+
data.port > 0 &&
245+
data.port < 65535 &&
246+
data.username.length > 0 &&
247+
data.password.length > 0 &&
248+
data.from.length > 0 &&
249+
/\w+@\w+\.\w+/.test(data.from) &&
250+
!data.username.includes("@")
251+
);
252+
}, [data]);
253+
241254
const onTest = async () => {
242255
if (!email.trim()) return;
243256
await onChange(false);
@@ -262,6 +275,11 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
262275
configParagraph={true}
263276
isCollapsed={true}
264277
>
278+
{!valid && (
279+
<ParagraphDescription border={true}>
280+
{t("admin.system.mailConfNotValid")}
281+
</ParagraphDescription>
282+
)}
265283
<ParagraphItem>
266284
<Label>
267285
<Require /> {t("admin.system.mailHost")}

app/src/store/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { configureStore } from "@reduxjs/toolkit";
2+
import infoReducer from "./info";
23
import globalReducer from "./globals";
34
import menuReducer from "./menu";
45
import authReducer from "./auth";
@@ -13,6 +14,7 @@ import settingsReducer from "./settings";
1314

1415
const store = configureStore({
1516
reducer: {
17+
info: infoReducer,
1618
global: globalReducer,
1719
menu: menuReducer,
1820
auth: authReducer,

app/src/store/info.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createSlice } from "@reduxjs/toolkit";
2+
import { InfoForm } from "@/events/info.ts";
3+
import { RootState } from "@/store/index.ts";
4+
5+
export const infoSlice = createSlice({
6+
name: "info",
7+
initialState: {
8+
mail: false,
9+
} as InfoForm,
10+
reducers: {
11+
setForm: (state, action) => {
12+
const form = action.payload as InfoForm;
13+
state.mail = form.mail ?? false;
14+
},
15+
},
16+
});
17+
18+
export const { setForm } = infoSlice.actions;
19+
20+
export default infoSlice.reducer;
21+
22+
export const infoDataSelector = (state: RootState): InfoForm => state.info;
23+
export const infoMailSelector = (state: RootState): boolean => state.info.mail;

auth/auth.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,13 @@ func SignUp(c *gin.Context, form RegisterForm) (string, error) {
106106
email := strings.TrimSpace(form.Email)
107107
code := strings.TrimSpace(form.Code)
108108

109+
enableVerify := channel.SystemInstance.IsMailValid()
110+
109111
if !utils.All(
110112
validateUsername(username),
111113
validatePassword(password),
112114
validateEmail(email),
113-
validateCode(code),
115+
!enableVerify || validateCode(code),
114116
) {
115117
return "", errors.New("invalid username/password/email format")
116118
}
@@ -127,7 +129,7 @@ func SignUp(c *gin.Context, form RegisterForm) (string, error) {
127129
return "", fmt.Errorf("email is already taken, please try another one email (your current email: %s)", email)
128130
}
129131

130-
if !checkCode(c, cache, email, code) {
132+
if enableVerify && !checkCode(c, cache, email, code) {
131133
return "", errors.New("invalid email verification code")
132134
}
133135

auth/controller.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type RegisterForm struct {
1212
Username string `form:"username" binding:"required"`
1313
Password string `form:"password" binding:"required"`
1414
Email string `form:"email" binding:"required"`
15-
Code string `form:"code" binding:"required"`
15+
Code string `form:"code"`
1616
}
1717

1818
type VerifyForm struct {

channel/system.go

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type ApiInfo struct {
1515
Docs string `json:"docs"`
1616
Announcement string `json:"announcement"`
1717
BuyLink string `json:"buy_link"`
18+
Mail bool `json:"mail"`
1819
}
1920

2021
type generalState struct {
@@ -87,6 +88,7 @@ func (c *SystemConfig) AsInfo() ApiInfo {
8788
Docs: c.General.Docs,
8889
Announcement: c.Site.Announcement,
8990
BuyLink: c.Site.BuyLink,
91+
Mail: c.IsMailValid(),
9092
}
9193
}
9294

0 commit comments

Comments
 (0)