Merge pull request #440 from answerdev/feat/1.1.1/ui

feat: Complete **SPAM** blocking of critical data apis.
This commit is contained in:
haitao.jarvis 2023-07-21 18:17:01 +08:00 committed by GitHub
commit 3423212b80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 970 additions and 769 deletions

View File

@ -9,6 +9,11 @@ export interface FormDataType {
[prop: string]: FormValue;
}
export interface FieldError {
error_field: string;
error_msg: string;
}
export interface Paging {
page: number;
page_size?: number;
@ -52,7 +57,7 @@ export interface TagInfo extends TagBase {
main_tag_slug_name?: string;
excerpt?;
}
export interface QuestionParams {
export interface QuestionParams extends ImgCodeReq{
title: string;
url_title?: string;
content: string;
@ -68,7 +73,7 @@ export interface ListResult<T = any> {
list: T[];
}
export interface AnswerParams {
export interface AnswerParams extends ImgCodeReq {
content: string;
html: string;
question_id: string;
@ -169,10 +174,29 @@ export interface PasswordResetReq extends ImgCodeReq {
e_mail: string;
}
export interface CheckImgReq {
action: 'login' | 'e_mail' | 'find_pass' | 'modify_pass';
export interface PasswordReplaceReq extends ImgCodeReq {
code: string;
pass: string;
}
export interface CaptchaReq extends ImgCodeReq {
verify: ImgCodeRes['verify'];
}
export type CaptchaKey =
| 'email'
| 'password'
| 'edit_userinfo'
| 'question'
| 'answer'
| 'comment'
| 'edit'
| 'invitation_answer'
| 'search'
| 'report'
| 'delete'
| 'vote';
export interface SetNoticeReq {
notice_switch: boolean;
}
@ -222,7 +246,7 @@ export interface AnswerItem {
[prop: string]: any;
}
export interface PostAnswerReq {
export interface PostAnswerReq extends ImgCodeReq {
content: string;
html?: string;
question_id: string;
@ -425,7 +449,7 @@ export interface FollowParams {
/**
* @description search request params
*/
export interface SearchParams {
export interface SearchParams extends ImgCodeReq {
q: string;
order: string;
page: number;

View File

@ -6,9 +6,10 @@ import classNames from 'classnames';
import { Icon } from '@/components';
import { loggedUserInfoStore } from '@/stores';
import { useToast } from '@/hooks';
import { useToast, useCaptchaModal } from '@/hooks';
import { tryNormalLogged } from '@/utils/guard';
import { bookmark, postVote } from '@/services';
import * as Types from '@/common/interface';
interface Props {
className?: string;
@ -36,6 +37,8 @@ const Index: FC<Props> = ({ className, data, source }) => {
const { username = '' } = loggedUserInfoStore((state) => state.user);
const toast = useToast();
const { t } = useTranslation();
const vCaptcha = useCaptchaModal('vote');
useEffect(() => {
if (data) {
setVotes(data.votesCount);
@ -61,27 +64,39 @@ const Index: FC<Props> = ({ className, data, source }) => {
return;
}
const isCancel = (type === 'up' && like) || (type === 'down' && hate);
postVote(
{
object_id: data?.id,
is_cancel: isCancel,
},
type,
)
.then((res) => {
setVotes(res.votes);
setLike(res.vote_status === 'vote_up');
setHated(res.vote_status === 'vote_down');
})
.catch((err) => {
const errMsg = err?.value;
if (errMsg) {
toast.onShow({
msg: errMsg,
variant: 'danger',
});
}
});
vCaptcha.check(() => {
const imgCode: Types.ImgCodeReq = {
captcha_id: undefined,
captcha_code: undefined,
};
vCaptcha.resolveCaptchaReq(imgCode);
postVote(
{
object_id: data?.id,
is_cancel: isCancel,
...imgCode,
},
type,
)
.then(async (res) => {
await vCaptcha.close();
setVotes(res.votes);
setLike(res.vote_status === 'vote_up');
setHated(res.vote_status === 'vote_down');
})
.catch((err) => {
if (err?.isError) {
vCaptcha.handleCaptchaError(err.list);
}
const errMsg = err?.value;
if (errMsg) {
toast.onShow({
msg: errMsg,
variant: 'danger',
});
}
});
});
};
const handleBookmark = () => {

View File

@ -8,7 +8,7 @@ import { unionBy } from 'lodash';
import * as Types from '@/common/interface';
import { Modal } from '@/components';
import { usePageUsers, useReportModal } from '@/hooks';
import { usePageUsers, useReportModal, useCaptchaModal } from '@/hooks';
import {
matchedUsers,
parseUserInfo,
@ -43,6 +43,11 @@ const Comment = ({ objectId, mode, commentId }) => {
const reportModal = useReportModal();
const addCaptcha = useCaptchaModal('comment');
const editCaptcha = useCaptchaModal('edit');
const dCaptcha = useCaptchaModal('delete');
const vCaptcha = useCaptchaModal('vote');
const { t } = useTranslation('translation', { keyPrefix: 'comment' });
const scrollCallback = useCallback((el, co) => {
if (pageIndex === 0 && co.comment_id === commentId) {
@ -120,43 +125,67 @@ const Comment = ({ objectId, mode, commentId }) => {
};
if (item.type === 'edit') {
return updateComment({
...params,
comment_id: item.comment_id,
}).then((res) => {
setComments(
comments.map((comment) => {
if (comment.comment_id === item.comment_id) {
comment.showEdit = false;
comment.parsed_text = res.parsed_text;
comment.original_text = res.original_text;
return editCaptcha.check(() => {
const up = {
...params,
comment_id: item.comment_id,
captcha_code: undefined,
captcha_id: undefined,
};
editCaptcha.resolveCaptchaReq(up);
return updateComment(up)
.then(async (res) => {
await editCaptcha.close();
setComments(
comments.map((comment) => {
if (comment.comment_id === item.comment_id) {
comment.showEdit = false;
comment.parsed_text = res.parsed_text;
comment.original_text = res.original_text;
}
return comment;
}),
);
})
.catch((err) => {
if (err.isError) {
editCaptcha.handleCaptchaError(err.list);
}
return comment;
}),
);
});
});
}
return addComment(params).then((res) => {
if (item.type === 'reply') {
const index = comments.findIndex(
(comment) => comment.comment_id === item.comment_id,
);
comments[index].showReply = false;
comments.splice(index + 1, 0, res);
setComments([...comments]);
} else {
setComments([
...comments.map((comment) => {
if (comment.comment_id === item.comment_id) {
comment.showReply = false;
}
return comment;
}),
res,
]);
}
setVisibleComment(false);
return addCaptcha.check(() => {
const req = {
...params,
captcha_code: undefined,
captcha_id: undefined,
};
addCaptcha.resolveCaptchaReq(req);
return addComment(req).then((res) => {
if (item.type === 'reply') {
const index = comments.findIndex(
(comment) => comment.comment_id === item.comment_id,
);
comments[index].showReply = false;
comments.splice(index + 1, 0, res);
setComments([...comments]);
} else {
setComments([
...comments.map((comment) => {
if (comment.comment_id === item.comment_id) {
comment.showReply = false;
}
return comment;
}),
res,
]);
}
setVisibleComment(false);
});
});
};
@ -167,11 +196,23 @@ const Comment = ({ objectId, mode, commentId }) => {
confirmBtnVariant: 'danger',
confirmText: t('delete', { keyPrefix: 'btns' }),
onConfirm: () => {
deleteComment(id).then(() => {
if (pageIndex === 0) {
mutate();
}
setComments(comments.filter((item) => item.comment_id !== id));
dCaptcha.check(() => {
const imgCode = { captcha_id: undefined, captcha_code: undefined };
dCaptcha.resolveCaptchaReq(imgCode);
deleteComment(id, imgCode)
.then(async () => {
await dCaptcha.close();
if (pageIndex === 0) {
mutate();
}
setComments(comments.filter((item) => item.comment_id !== id));
})
.catch((ex) => {
if (ex.isError) {
dCaptcha.handleCaptchaError(ex.list);
}
});
});
},
});
@ -182,24 +223,40 @@ const Comment = ({ objectId, mode, commentId }) => {
return;
}
postVote(
{
object_id: id,
is_cancel,
},
'up',
).then(() => {
setComments(
comments.map((item) => {
if (item.comment_id === id) {
item.vote_count = is_cancel
? item.vote_count - 1
: item.vote_count + 1;
item.is_vote = !is_cancel;
vCaptcha.check(() => {
const imgCode: Types.ImgCodeReq = {
captcha_id: undefined,
captcha_code: undefined,
};
vCaptcha.resolveCaptchaReq(imgCode);
postVote(
{
object_id: id,
is_cancel,
...imgCode,
},
'up',
)
.then(async () => {
await vCaptcha.close();
setComments(
comments.map((item) => {
if (item.comment_id === id) {
item.vote_count = is_cancel
? item.vote_count - 1
: item.vote_count + 1;
item.is_vote = !is_cancel;
}
return item;
}),
);
})
.catch((ex) => {
if (ex.isError) {
vCaptcha.handleCaptchaError(ex.list);
}
return item;
}),
);
});
});
};

View File

@ -4,7 +4,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components';
import { useReportModal, useToast } from '@/hooks';
import { useReportModal, useToast, useCaptchaModal } from '@/hooks';
import { QuestionOperationReq } from '@/common/interface';
import Share from '../Share';
import {
@ -44,6 +44,7 @@ const Index: FC<IProps> = ({
const toast = useToast();
const navigate = useNavigate();
const reportModal = useReportModal();
const dCaptcha = useCaptchaModal('delete');
const refreshQuestion = () => {
callback?.('default');
@ -77,14 +78,28 @@ const Index: FC<IProps> = ({
confirmBtnVariant: 'danger',
confirmText: t('delete', { keyPrefix: 'btns' }),
onConfirm: () => {
deleteQuestion({
id: qid,
}).then(() => {
toast.onShow({
msg: t('post_deleted', { keyPrefix: 'messages' }),
variant: 'success',
});
callback?.('delete_question');
dCaptcha.check(() => {
const req = {
id: qid,
captcha_code: undefined,
captcha_id: undefined,
};
dCaptcha.resolveCaptchaReq(req);
deleteQuestion(req)
.then(async () => {
await dCaptcha.close();
toast.onShow({
msg: t('post_deleted', { keyPrefix: 'messages' }),
variant: 'success',
});
callback?.('delete_question');
})
.catch((ex) => {
if (ex.isError) {
dCaptcha.handleCaptchaError(ex.list);
}
});
});
},
});
@ -98,15 +113,29 @@ const Index: FC<IProps> = ({
confirmBtnVariant: 'danger',
confirmText: t('delete', { keyPrefix: 'btns' }),
onConfirm: () => {
deleteAnswer({
id: aid,
}).then(() => {
// refresh page
toast.onShow({
msg: t('tip_answer_deleted'),
variant: 'success',
});
callback?.('all');
dCaptcha.check(() => {
const req = {
id: aid,
captcha_code: undefined,
captcha_id: undefined,
};
dCaptcha.resolveCaptchaReq(req);
deleteAnswer(req)
.then(async () => {
await dCaptcha.close();
// refresh page
toast.onShow({
msg: t('tip_answer_deleted'),
variant: 'success',
});
callback?.('all');
})
.catch((ex) => {
if (ex.isError) {
dCaptcha.handleCaptchaError(ex.list);
}
});
});
},
});

View File

@ -1,24 +1,21 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { Button, Col } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { PicAuthCodeModal } from '@/components/Modal';
import type { ImgCodeRes, ImgCodeReq, FormDataType } from '@/common/interface';
import type { ImgCodeReq, FormDataType } from '@/common/interface';
import { loggedUserInfoStore } from '@/stores';
import { resendEmail, checkImgCode } from '@/services';
import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants';
import Storage from '@/utils/storage';
import { resendEmail } from '@/services';
import { handleFormError } from '@/utils';
import { useCaptchaModal } from '@/hooks';
interface IProps {
visible: boolean;
visible?: boolean;
}
const Index: React.FC<IProps> = ({ visible = false }) => {
const Index: React.FC<IProps> = () => {
const { t } = useTranslation('translation', { keyPrefix: 'inactive' });
const [isSuccess, setSuccess] = useState(false);
const [showModal, setModalState] = useState(false);
const { e_mail } = loggedUserInfoStore((state) => state.user);
const [formData, setFormData] = useState<FormDataType>({
captcha_code: {
@ -27,75 +24,39 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
errorMsg: '',
},
});
const [imgCode, setImgCode] = useState<ImgCodeRes>({
captcha_id: '',
captcha_img: '',
verify: false,
});
const getImgCode = () => {
checkImgCode({
action: 'e_mail',
}).then((res) => {
setImgCode(res);
});
};
const emailCaptcha = useCaptchaModal('email');
const submit = (e?: any) => {
if (e) {
e.preventDefault();
}
let obj: ImgCodeReq = {};
const submit = () => {
let req: ImgCodeReq = {};
const imgCode = emailCaptcha.getCaptcha();
if (imgCode.verify) {
const code = Storage.get(CAPTCHA_CODE_STORAGE_KEY) || '';
obj = {
captcha_code: code,
req = {
captcha_code: imgCode.captcha_code,
captcha_id: imgCode.captcha_id,
};
}
resendEmail(obj)
resendEmail(req)
.then(() => {
emailCaptcha.close();
setSuccess(true);
setModalState(false);
})
.catch((err) => {
if (err.isError) {
emailCaptcha.handleCaptchaError(err.list);
const data = handleFormError(err, formData);
setFormData({ ...data });
}
})
.finally(() => {
getImgCode();
});
};
const onSentEmail = () => {
if (imgCode.verify) {
setModalState(true);
if (!formData.captcha_code.value) {
setFormData({
captcha_code: {
value: '',
isInvalid: false,
errorMsg: t('msg.empty'),
},
});
}
return;
}
submit();
const onSentEmail = (evt) => {
evt.preventDefault();
emailCaptcha.check(() => {
submit();
});
};
const handleChange = (params: FormDataType) => {
setFormData({ ...formData, ...params });
};
useEffect(() => {
if (visible) {
getImgCode();
}
}, [visible]);
return (
<Col md={6} className="mx-auto text-center">
{isSuccess ? (
@ -124,18 +85,6 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
</Link>
</>
)}
<PicAuthCodeModal
visible={showModal}
data={{
captcha: formData.captcha_code,
imgCode,
}}
handleCaptcha={handleChange}
clickSubmit={submit}
refreshImgCode={getImgCode}
onClose={() => setModalState(false)}
/>
</Col>
);
};

View File

@ -11,6 +11,7 @@ import usePageTags from './usePageTags';
import useLoginRedirect from './useLoginRedirect';
import usePromptWithUnload from './usePrompt';
import useActivationEmailModal from './useActivationEmailModal';
import useCaptchaModal from './useCaptchaModal';
export {
useTagModal,
@ -26,4 +27,5 @@ export {
useLoginRedirect,
usePromptWithUnload,
useActivationEmailModal,
useCaptchaModal,
};

View File

@ -0,0 +1,253 @@
import { useEffect, useRef, useState, useLayoutEffect } from 'react';
import { Modal, Form, Button, InputGroup } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import ReactDOM from 'react-dom/client';
import { Icon } from '@/components';
import type {
FormValue,
ImgCodeRes,
CaptchaKey,
FieldError,
ImgCodeReq,
} from '@/common/interface';
import { checkImgCode } from '@/services';
type SubmitCallback = {
(): void;
};
const Index = (captchaKey: CaptchaKey) => {
const refRoot = useRef(null);
if (refRoot.current === null) {
// @ts-ignore
refRoot.current = ReactDOM.createRoot(document.createElement('div'));
}
const { t } = useTranslation('translation', { keyPrefix: 'pic_auth_code' });
const refKey = useRef<CaptchaKey>(captchaKey);
const refCallback = useRef<SubmitCallback>();
const pending = useRef(false);
const autoInitCaptchaData = /email/i.test(refKey.current);
const [stateShow, setStateShow] = useState(false);
const [captcha, setCaptcha] = useState<ImgCodeRes>({
captcha_id: '',
captcha_img: '',
verify: false,
});
const [imgCode, setImgCode] = useState<FormValue>({
value: '',
isInvalid: false,
errorMsg: '',
});
const refCaptcha = useRef(captcha);
const refImgCode = useRef(imgCode);
const fetchCaptchaData = () => {
pending.current = true;
checkImgCode(refKey.current)
.then((resp) => {
setCaptcha(resp);
})
.finally(() => {
pending.current = false;
});
};
const resetCapture = () => {
setCaptcha({
captcha_id: '',
captcha_img: '',
verify: false,
});
};
const show = () => {
if (!stateShow) {
setStateShow(true);
}
};
/**
* There are some cases where the React scheduler cancels the execution of some functions,
* which prevents them from closing properly:
* for example, if the parent component uninstalls the child component directly,
* and the `captchaModal.close()` call is inside the child component.
* In this case, call `await captchaModal.close()` and wait for the close action to complete.
*/
const close = (reset = true) => {
setStateShow(false);
if (reset) {
resetCapture();
}
const p = new Promise<void>((resolve) => {
setTimeout(resolve);
});
return p;
};
const handleCaptchaError = (fel: FieldError[] = []) => {
const captchaErr = fel.find((o) => {
return o.error_field === 'captcha_code';
});
const ri = refImgCode.current;
if (captchaErr) {
/**
* `imgCode.value` No value but a validation error is received,
* indicating that it is the first time the interface has returned a CAPTCHA error,
* triggering the CAPTCHA logic. There is no need to display the error message at this point.
*/
if (ri.value) {
setImgCode({
...ri,
isInvalid: true,
errorMsg: captchaErr.error_msg,
});
}
fetchCaptchaData();
} else {
setImgCode({
...ri,
isInvalid: false,
errorMsg: '',
});
close();
}
};
const handleChange = (evt) => {
evt.preventDefault();
setImgCode({
value: evt.target.value || '',
isInvalid: false,
errorMsg: '',
});
};
const getCaptcha = () => {
const rc = refCaptcha.current;
const ri = refImgCode.current;
const r = {
verify: !!rc?.verify,
captcha_id: rc?.captcha_id,
captcha_code: ri.value,
};
return r;
};
const resolveCaptchaReq = (req: ImgCodeReq) => {
const r = getCaptcha();
if (r.verify) {
req.captcha_code = r.captcha_code;
req.captcha_id = r.captcha_id;
}
};
const handleSubmit = (evt) => {
evt.preventDefault();
if (!imgCode.value) {
return;
}
if (refCallback.current) {
refCallback.current();
}
};
useEffect(() => {
if (autoInitCaptchaData) {
fetchCaptchaData();
}
}, []);
useLayoutEffect(() => {
refImgCode.current = imgCode;
refCaptcha.current = captcha;
}, [captcha, imgCode]);
useEffect(() => {
// @ts-ignore
refRoot.current.render(
<Modal
size="sm"
title="Captcha"
show={stateShow}
onHide={() => close(false)}
centered>
<Modal.Header closeButton>
<Modal.Title as="h5">{t('title')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="code" className="mb-3">
<div className="mb-3 p-2 d-flex align-items-center justify-content-center bg-light rounded-2">
<img
src={captcha?.captcha_img}
alt="captcha img"
width="auto"
height="40px"
/>
</div>
<InputGroup>
<Form.Control
type="text"
autoComplete="off"
placeholder={t('placeholder')}
isInvalid={imgCode?.isInvalid}
onChange={handleChange}
value={imgCode.value}
/>
<Button
onClick={fetchCaptchaData}
variant="outline-secondary"
title={t('refresh', { keyPrefix: 'btns' })}
style={{
borderTopRightRadius: '0.375rem',
borderBottomRightRadius: '0.375rem',
}}>
<Icon name="arrow-repeat" />
</Button>
<Form.Control.Feedback type="invalid">
{imgCode?.errorMsg}
</Form.Control.Feedback>
</InputGroup>
</Form.Group>
<div className="d-grid">
<Button type="submit" disabled={!imgCode.value}>
{t('verify', { keyPrefix: 'btns' })}
</Button>
</div>
</Form>
</Modal.Body>
</Modal>,
);
});
const r = {
close,
show,
check: (submitFunc: SubmitCallback) => {
if (pending.current) {
return false;
}
refCallback.current = submitFunc;
if (captcha?.verify) {
show();
}
return submitFunc();
},
getCaptcha,
resolveCaptchaReq,
fetchCaptchaData,
handleCaptchaError,
};
return r;
};
export default Index;

View File

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import ReactDOM from 'react-dom/client';
import { useToast } from '@/hooks';
import { useToast, useCaptchaModal } from '@/hooks';
import type * as Type from '@/common/interface';
import { reportList, postReport, closeQuestion, putReport } from '@/services';
@ -37,6 +37,8 @@ const useReportModal = (callback?: () => void) => {
const [show, setShow] = useState(false);
const [list, setList] = useState<any[]>([]);
const rCaptcha = useCaptchaModal('report');
useEffect(() => {
const div = document.createElement('div');
rootRef.current.root = ReactDOM.createRoot(div);
@ -103,18 +105,32 @@ const useReportModal = (callback?: () => void) => {
return;
}
if (!params.isBackend && params.action === 'flag') {
postReport({
source: params.type,
report_type: reportType.type,
object_id: params.id,
content: content.value,
}).then(() => {
toast.onShow({
msg: t('flag_success', { keyPrefix: 'toast' }),
variant: 'warning',
});
onClose();
asyncCallback();
rCaptcha.check(() => {
const flagReq = {
source: params.type,
report_type: reportType.type,
object_id: params.id,
content: content.value,
captcha_code: undefined,
captcha_id: undefined,
};
rCaptcha.resolveCaptchaReq(flagReq);
postReport(flagReq)
.then(async () => {
await rCaptcha.close();
toast.onShow({
msg: t('flag_success', { keyPrefix: 'toast' }),
variant: 'warning',
});
onClose();
asyncCallback();
})
.catch((ex) => {
if (ex.isError) {
rCaptcha.handleCaptchaError(ex.list);
}
});
});
}

View File

@ -7,7 +7,7 @@ import dayjs from 'dayjs';
import classNames from 'classnames';
import { isEqual } from 'lodash';
import { usePageTags, usePromptWithUnload } from '@/hooks';
import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks';
import { Editor, EditorRef, TagSelector } from '@/components';
import type * as Type from '@/common/interface';
import { DRAFT_QUESTION_STORAGE_KEY } from '@/common/constants';
@ -102,6 +102,9 @@ const Ask = () => {
isEdit ? '' : formData.title.value,
);
const saveCaptcha = useCaptchaModal('question');
const editCaptcha = useCaptchaModal('edit');
const removeDraft = () => {
saveDraft.save.cancel();
saveDraft.remove();
@ -251,52 +254,69 @@ const Ask = () => {
tags: formData.tags.value,
};
if (isEdit) {
modifyQuestion({
...params,
id: qid,
edit_summary: formData.edit_summary.value,
})
.then((res) => {
navigate(pathFactory.questionLanding(qid, params.url_title), {
state: { isReview: res?.wait_for_review },
});
})
.catch((err) => {
if (err.isError) {
const data = handleFormError(err, formData);
setFormData({ ...data });
}
});
} else {
let res;
if (checked) {
res = await saveQuestionWidthAnaser({
editCaptcha.check(() => {
const ep = {
...params,
answer_content: formData.answer_content.value,
}).catch((err) => {
if (err.isError) {
const data = handleFormError(err, formData);
setFormData({ ...data });
}
});
} else {
res = await saveQuestion(params).catch((err) => {
if (err.isError) {
const data = handleFormError(err, formData);
setFormData({ ...data });
}
});
}
const id = res?.id || res?.question?.id;
if (id) {
if (checked) {
navigate(pathFactory.questionLanding(id, res?.question?.url_title));
} else {
navigate(pathFactory.questionLanding(id));
id: qid,
edit_summary: formData.edit_summary.value,
};
const imgCode = editCaptcha.getCaptcha();
if (imgCode.verify) {
ep.captcha_code = imgCode.captcha_code;
ep.captcha_id = imgCode.captcha_id;
}
}
removeDraft();
modifyQuestion(ep)
.then(async (res) => {
await editCaptcha.close();
navigate(pathFactory.questionLanding(qid, params.url_title), {
state: { isReview: res?.wait_for_review },
});
})
.catch((err) => {
if (err.isError) {
editCaptcha.handleCaptchaError(err.list);
const data = handleFormError(err, formData);
setFormData({ ...data });
}
});
});
} else {
saveCaptcha.check(async () => {
const imgCode = saveCaptcha.getCaptcha();
if (imgCode.verify) {
params.captcha_code = imgCode.captcha_code;
params.captcha_id = imgCode.captcha_id;
}
let res;
if (checked) {
res = await saveQuestionWidthAnaser({
...params,
answer_content: formData.answer_content.value,
}).catch((err) => {
if (err.isError) {
const data = handleFormError(err, formData);
setFormData({ ...data });
}
});
} else {
res = await saveQuestion(params).catch((err) => {
if (err.isError) {
const data = handleFormError(err, formData);
setFormData({ ...data });
}
});
}
const id = res?.id || res?.question?.id;
if (id) {
if (checked) {
navigate(pathFactory.questionLanding(id, res?.question?.url_title));
} else {
navigate(pathFactory.questionLanding(id));
}
}
removeDraft();
});
}
};
const backPage = () => {

View File

@ -8,6 +8,7 @@ import classNames from 'classnames';
import { Avatar } from '@/components';
import { getInviteUser, putInviteUser } from '@/services';
import type * as Type from '@/common/interface';
import { useCaptchaModal } from '@/hooks';
import PeopleDropdown from './PeopleDropdown';
@ -22,6 +23,7 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
const MAX_ASK_NUMBER = 5;
const [editing, setEditing] = useState(false);
const [users, setUsers] = useState<Type.UserInfoBase[]>();
const iaCaptcha = useCaptchaModal('invitation_answer');
const initInviteUsers = () => {
if (!questionId) {
@ -60,14 +62,23 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
const names = users.map((_) => {
return _.username;
});
putInviteUser(questionId, names)
.then(() => {
setEditing(false);
})
.catch((ex) => {
console.log('ex: ', ex);
});
iaCaptcha.check(() => {
const imgCode: Type.ImgCodeReq = {};
iaCaptcha.resolveCaptchaReq(imgCode);
putInviteUser(questionId, names, imgCode)
.then(async () => {
await iaCaptcha.close();
setEditing(false);
})
.catch((ex) => {
if (ex.isError) {
iaCaptcha.handleCaptchaError(ex.list);
}
console.log('ex: ', ex);
});
});
};
useEffect(() => {
initInviteUsers();
}, [questionId]);

View File

@ -5,9 +5,9 @@ import { useTranslation, Trans } from 'react-i18next';
import { marked } from 'marked';
import classNames from 'classnames';
import { usePromptWithUnload } from '@/hooks';
import { usePromptWithUnload, useCaptchaModal } from '@/hooks';
import { Editor, Modal, TextArea } from '@/components';
import { FormDataType } from '@/common/interface';
import { FormDataType, PostAnswerReq } from '@/common/interface';
import { postAnswer } from '@/services';
import { guard, handleFormError, SaveDraft, storageExpires } from '@/utils';
import { DRAFT_ANSWER_STORAGE_KEY } from '@/common/constants';
@ -41,6 +41,7 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
const [editorFocusState, setEditorFocusState] = useState(false);
const [hasDraft, setHasDraft] = useState(false);
const [showTips, setShowTips] = useState(data.loggedUserRank < 100);
const aCaptcha = useCaptchaModal('answer');
usePromptWithUnload({
when: Boolean(formData.content.value),
@ -135,29 +136,40 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
if (!checkValidated()) {
return;
}
postAnswer({
question_id: data?.qid,
content: formData.content.value,
html: marked.parse(formData.content.value),
})
.then((res) => {
setShowEditor(false);
setFormData({
content: {
value: '',
isInvalid: false,
errorMsg: '',
},
aCaptcha.check(() => {
const params: PostAnswerReq = {
question_id: data?.qid,
content: formData.content.value,
html: marked.parse(formData.content.value),
};
const imgCode = aCaptcha.getCaptcha();
if (imgCode.verify) {
params.captcha_code = imgCode.captcha_code;
params.captcha_id = imgCode.captcha_id;
}
postAnswer(params)
.then(async (res) => {
await aCaptcha.close();
setShowEditor(false);
setFormData({
content: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
removeDraft();
callback?.(res.info);
})
.catch((ex) => {
if (ex.isError) {
aCaptcha.handleCaptchaError(ex.list);
const stateData = handleFormError(ex, formData);
setFormData({ ...stateData });
}
});
removeDraft();
callback?.(res.info);
})
.catch((ex) => {
if (ex.isError) {
const stateData = handleFormError(ex, formData);
setFormData({ ...stateData });
}
});
});
};
const clickBtn = () => {

View File

@ -7,7 +7,7 @@ import dayjs from 'dayjs';
import classNames from 'classnames';
import { handleFormError, scrollToDocTop } from '@/utils';
import { usePageTags, usePromptWithUnload } from '@/hooks';
import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks';
import { pathFactory } from '@/router/pathFactory';
import { Editor, EditorRef, Icon, htmlRender } from '@/components';
import type * as Type from '@/common/interface';
@ -51,6 +51,7 @@ const Index = () => {
const [formData, setFormData] = useState<FormDataItem>(initFormData);
const [immData, setImmData] = useState(initFormData);
const [contentChanged, setContentChanged] = useState(false);
const editCaptcha = useCaptchaModal('edit');
useLayoutEffect(() => {
if (data?.info?.content) {
@ -136,36 +137,43 @@ const Index = () => {
event.preventDefault();
event.stopPropagation();
if (!checkValidated()) {
return;
}
const params: Type.AnswerParams = {
content: formData.content.value,
html: editorRef.current.getHtml(),
question_id: qid,
id: aid,
edit_summary: formData.description.value,
};
modifyAnswer(params)
.then((res) => {
navigate(
pathFactory.answerLanding({
questionId: qid,
slugTitle: data?.question?.url_title,
answerId: aid,
}),
{
state: { isReview: res?.wait_for_review },
},
);
})
.catch((ex) => {
if (ex.isError) {
const stateData = handleFormError(ex, formData);
setFormData({ ...stateData });
}
});
editCaptcha.check(() => {
const params: Type.AnswerParams = {
content: formData.content.value,
html: editorRef.current.getHtml(),
question_id: qid,
id: aid,
edit_summary: formData.description.value,
};
editCaptcha.resolveCaptchaReq(params);
modifyAnswer(params)
.then(async (res) => {
await editCaptcha.close();
navigate(
pathFactory.answerLanding({
questionId: qid,
slugTitle: data?.question?.url_title,
answerId: aid,
}),
{
state: { isReview: res?.wait_for_review },
},
);
})
.catch((ex) => {
if (ex.isError) {
editCaptcha.handleCaptchaError(ex.list);
const stateData = handleFormError(ex, formData);
setFormData({ ...stateData });
}
});
});
};
const handleSelectedRevision = (e) => {
const index = e.target.value;

View File

@ -1,10 +1,12 @@
import { Row, Col, ListGroup } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { usePageTags } from '@/hooks';
import { usePageTags, useCaptchaModal } from '@/hooks';
import { Pagination } from '@/components';
import { useSearch } from '@/services';
import { getSearchResult } from '@/services';
import type { SearchParams, SearchRes } from '@/common/interface';
import {
Head,
@ -21,15 +23,52 @@ const Index = () => {
const page = searchParams.get('page') || 1;
const q = searchParams.get('q') || '';
const order = searchParams.get('order') || 'active';
const { data, isLoading } = useSearch({
q,
order,
page: Number(page),
size: 20,
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<SearchRes>({
count: 0,
list: [],
extra: null,
});
const { count = 0, list = [], extra = null } = data || {};
const searchCaptcha = useCaptchaModal('search');
const doSearch = () => {
setIsLoading(true);
const params: SearchParams = {
q,
order,
page: Number(page),
size: 20,
};
const captcha = searchCaptcha.getCaptcha();
if (captcha?.verify) {
params.captcha_id = captcha.captcha_id;
params.captcha_code = captcha.captcha_code;
}
getSearchResult(params)
.then((resp) => {
searchCaptcha.close();
setData(resp);
})
.catch((err) => {
if (err.isError) {
searchCaptcha.handleCaptchaError(err.list);
}
})
.finally(() => {
setIsLoading(false);
});
};
useEffect(() => {
searchCaptcha.check(() => {
doSearch();
});
}, [q, order, page]);
let pageTitle = t('search', { keyPrefix: 'page_title' });
if (q) {
pageTitle = `${t('posts_containing', { keyPrefix: 'page_title' })} '${q}'`;
@ -37,6 +76,7 @@ const Index = () => {
usePageTags({
title: pageTitle,
});
return (
<Row className="pt-4 mb-5">
<Col className="page-main flex-auto">

View File

@ -1,22 +1,19 @@
import { FC, memo, useEffect, useState } from 'react';
import { FC, memo, useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import type {
ImgCodeRes,
PasswordResetReq,
FormDataType,
} from '@/common/interface';
import { resetPassword, checkImgCode } from '@/services';
import { PicAuthCodeModal } from '@/components/Modal';
import type { PasswordResetReq, FormDataType } from '@/common/interface';
import { resetPassword } from '@/services';
import { handleFormError } from '@/utils';
import { useCaptchaModal } from '@/hooks';
interface IProps {
visible: boolean;
// eslint-disable-next-line react/no-unused-prop-types
visible?: boolean;
callback: (param: number, email: string) => void;
}
const Index: FC<IProps> = ({ visible = false, callback }) => {
const Index: FC<IProps> = ({ callback }) => {
const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' });
const [formData, setFormData] = useState<FormDataType>({
e_mail: {
@ -24,26 +21,9 @@ const Index: FC<IProps> = ({ visible = false, callback }) => {
isInvalid: false,
errorMsg: '',
},
captcha_code: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
const [imgCode, setImgCode] = useState<ImgCodeRes>({
captcha_id: '',
captcha_img: '',
verify: false,
});
const [showModal, setModalState] = useState(false);
const getImgCode = () => {
checkImgCode({
action: 'find_pass',
}).then((res) => {
setImgCode(res);
});
};
const emailCaptcha = useCaptchaModal('email');
const handleChange = (params: FormDataType) => {
setFormData({ ...formData, ...params });
@ -73,27 +53,24 @@ const Index: FC<IProps> = ({ visible = false, callback }) => {
const params: PasswordResetReq = {
e_mail: formData.e_mail.value,
};
if (imgCode.verify) {
params.captcha_code = formData.captcha_code.value;
params.captcha_id = imgCode.captcha_id;
const captcha = emailCaptcha.getCaptcha();
if (captcha.verify) {
params.captcha_code = captcha.captcha_code;
params.captcha_id = captcha.captcha_id;
}
resetPassword(params)
.then(() => {
.then(async () => {
await emailCaptcha.close();
callback?.(2, formData.e_mail.value);
setModalState(false);
})
.catch((err) => {
if (err.isError) {
emailCaptcha.handleCaptchaError(err.list);
const data = handleFormError(err, formData);
if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
setModalState(false);
}
setFormData({ ...data });
}
})
.finally(() => {
getImgCode();
});
};
@ -105,64 +82,41 @@ const Index: FC<IProps> = ({ visible = false, callback }) => {
return;
}
if (imgCode.verify) {
setModalState(true);
return;
}
sendEmail();
emailCaptcha.check(() => {
sendEmail();
});
};
useEffect(() => {
if (visible) {
getImgCode();
}
}, [visible]);
return (
<>
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('email.label')}</Form.Label>
<Form.Control
required
type="email"
value={formData.e_mail.value}
isInvalid={formData.e_mail.isInvalid}
onChange={(e) => {
handleChange({
e_mail: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.e_mail.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('email.label')}</Form.Label>
<Form.Control
required
type="email"
value={formData.e_mail.value}
isInvalid={formData.e_mail.isInvalid}
onChange={(e) => {
handleChange({
e_mail: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.e_mail.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<div className="d-grid mb-3">
<Button variant="primary" type="submit">
{t('btn_name')}
</Button>
</div>
</Form>
<PicAuthCodeModal
visible={showModal}
data={{
captcha: formData.captcha_code,
imgCode,
}}
handleCaptcha={handleChange}
clickSubmit={sendEmail}
refreshImgCode={getImgCode}
onClose={() => setModalState(false)}
/>
</>
<div className="d-grid mb-3">
<Button variant="primary" type="submit">
{t('btn_name')}
</Button>
</div>
</Form>
);
};

View File

@ -1,17 +1,13 @@
import { FC, memo, useEffect, useState } from 'react';
import { FC, memo, useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import type {
ImgCodeRes,
PasswordResetReq,
FormDataType,
} from '@/common/interface';
import type { PasswordResetReq, FormDataType } from '@/common/interface';
import { loggedUserInfoStore } from '@/stores';
import { changeEmail, checkImgCode } from '@/services';
import { PicAuthCodeModal } from '@/components/Modal';
import { changeEmail } from '@/services';
import { handleFormError } from '@/utils';
import { useCaptchaModal } from '@/hooks';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
@ -21,28 +17,12 @@ const Index: FC = () => {
isInvalid: false,
errorMsg: '',
},
captcha_code: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
const [imgCode, setImgCode] = useState<ImgCodeRes>({
captcha_id: '',
captcha_img: '',
verify: false,
});
const [showModal, setModalState] = useState(false);
const navigate = useNavigate();
const { user: userInfo, update: updateUser } = loggedUserInfoStore();
const getImgCode = () => {
checkImgCode({
action: 'e_mail',
}).then((res) => {
setImgCode(res);
});
};
const emailCaptcha = useCaptchaModal('email');
const handleChange = (params: FormDataType) => {
setFormData({ ...formData, ...params });
@ -72,28 +52,25 @@ const Index: FC = () => {
const params: PasswordResetReq = {
e_mail: formData.e_mail.value,
};
const imgCode = emailCaptcha.getCaptcha();
if (imgCode.verify) {
params.captcha_code = formData.captcha_code.value;
params.captcha_code = imgCode.captcha_code;
params.captcha_id = imgCode.captcha_id;
}
changeEmail(params)
.then(() => {
.then(async () => {
await emailCaptcha.close();
userInfo.e_mail = formData.e_mail.value;
updateUser(userInfo);
navigate('/users/login', { replace: true });
setModalState(false);
})
.catch((err) => {
if (err.isError) {
emailCaptcha.handleCaptchaError(err.list);
const data = handleFormError(err, formData);
if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
setModalState(false);
}
setFormData({ ...data });
}
})
.finally(() => {
getImgCode();
});
};
@ -104,69 +81,48 @@ const Index: FC = () => {
return;
}
if (imgCode.verify) {
setModalState(true);
return;
}
sendEmail();
emailCaptcha.check(() => {
sendEmail();
});
};
const goBack = () => {
navigate('/users/login?status=inactive', { replace: true });
};
useEffect(() => {
getImgCode();
}, []);
return (
<>
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('email.label')}</Form.Label>
<Form.Control
required
type="email"
value={formData.e_mail.value}
isInvalid={formData.e_mail.isInvalid}
onChange={(e) => {
handleChange({
e_mail: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.e_mail.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('email.label')}</Form.Label>
<Form.Control
required
type="email"
value={formData.e_mail.value}
isInvalid={formData.e_mail.isInvalid}
onChange={(e) => {
handleChange({
e_mail: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Control.Feedback type="invalid">
{formData.e_mail.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<div className="d-grid mb-3">
<Button variant="primary" type="submit">
{t('btn_update')}
</Button>
<Button variant="link" className="mt-2 d-block" onClick={goBack}>
{t('btn_cancel')}
</Button>
</div>
</Form>
<PicAuthCodeModal
visible={showModal}
data={{
captcha: formData.captcha_code,
imgCode,
}}
handleCaptcha={handleChange}
clickSubmit={sendEmail}
refreshImgCode={getImgCode}
onClose={() => setModalState(false)}
/>
</>
<div className="d-grid mb-3">
<Button variant="primary" type="submit">
{t('btn_update')}
</Button>
<Button variant="link" className="mt-2 d-block" onClick={goBack}>
{t('btn_cancel')}
</Button>
</div>
</Form>
);
};

View File

@ -3,12 +3,8 @@ import { Container, Form, Button, Col } from 'react-bootstrap';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import type {
LoginReqParams,
ImgCodeRes,
FormDataType,
} from '@/common/interface';
import { usePageTags, useCaptchaModal } from '@/hooks';
import type { LoginReqParams, FormDataType } from '@/common/interface';
import { Unactivate, WelcomeTitle, PluginRender } from '@/components';
import {
loggedUserInfoStore,
@ -16,14 +12,12 @@ import {
userCenterStore,
} from '@/stores';
import { floppyNavigation, guard, handleFormError, userCenter } from '@/utils';
import { login, checkImgCode, UcAgent } from '@/services';
import { PicAuthCodeModal } from '@/components/Modal';
import { login, UcAgent } from '@/services';
const Index: React.FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'login' });
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [refresh, setRefresh] = useState(0);
const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _);
const loginSetting = loginSettingStore((state) => state.login);
const ucAgent = userCenterStore().agent;
@ -45,34 +39,15 @@ const Index: React.FC = () => {
isInvalid: false,
errorMsg: '',
},
captcha_code: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
const [imgCode, setImgCode] = useState<ImgCodeRes>({
captcha_id: '',
captcha_img: '',
verify: false,
});
const [showModal, setModalState] = useState(false);
const [step, setStep] = useState(1);
const handleChange = (params: FormDataType) => {
setFormData({ ...formData, ...params });
};
const getImgCode = () => {
if (!canOriginalLogin) {
return;
}
checkImgCode({
action: 'login',
}).then((res) => {
setImgCode(res);
});
};
const passwordCaptcha = useCaptchaModal('password');
const checkValidated = (): boolean => {
let bol = true;
@ -110,34 +85,31 @@ const Index: React.FC = () => {
e_mail: formData.e_mail.value,
pass: formData.pass.value,
};
if (imgCode.verify) {
params.captcha_code = formData.captcha_code.value;
params.captcha_id = imgCode.captcha_id;
const captcha = passwordCaptcha.getCaptcha();
if (captcha?.verify) {
params.captcha_code = captcha.captcha_code;
params.captcha_id = captcha.captcha_id;
}
login(params)
.then((res) => {
passwordCaptcha.close();
updateUser(res);
const userStat = guard.deriveLoginState();
if (userStat.isNotActivated) {
// inactive
setStep(2);
setRefresh((pre) => pre + 1);
} else {
guard.handleLoginRedirect(navigate);
}
setModalState(false);
})
.catch((err) => {
if (err.isError) {
const data = handleFormError(err, formData);
if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
setModalState(false);
}
setFormData({ ...data });
passwordCaptcha.handleCaptchaError(err.list);
}
setRefresh((pre) => pre + 1);
});
};
@ -149,18 +121,11 @@ const Index: React.FC = () => {
return;
}
if (imgCode.verify) {
setModalState(true);
return;
}
handleLogin();
passwordCaptcha.check(() => {
handleLogin();
});
};
useEffect(() => {
getImgCode();
}, [refresh]);
useEffect(() => {
const isInactive = searchParams.get('status');
@ -168,6 +133,7 @@ const Index: React.FC = () => {
setStep(2);
}
}, []);
usePageTags({
title: t('login', { keyPrefix: 'page_title' }),
});
@ -263,18 +229,6 @@ const Index: React.FC = () => {
) : null}
{step === 2 && <Unactivate visible={step === 2} />}
<PicAuthCodeModal
visible={showModal}
data={{
captcha: formData.captcha_code,
imgCode,
}}
handleCaptcha={handleChange}
clickSubmit={handleLogin}
refreshImgCode={getImgCode}
onClose={() => setModalState(false)}
/>
</Container>
);
};

View File

@ -1,17 +1,11 @@
import React, { FormEvent, MouseEvent, useEffect, useState } from 'react';
import React, { FormEvent, MouseEvent, useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { Trans, useTranslation } from 'react-i18next';
import { PicAuthCodeModal } from '@/components/Modal';
import { ImgCodeRes } from '@/common/interface';
import { useCaptchaModal } from '@/hooks';
import type { FormDataType, RegisterReqParams } from '@/common/interface';
import {
register,
getRegisterCaptcha,
useLegalTos,
useLegalPrivacy,
} from '@/services';
import { register, useLegalTos, useLegalPrivacy } from '@/services';
import userStore from '@/stores/loggedUserInfo';
import { handleFormError } from '@/utils';
@ -37,25 +31,11 @@ const Index: React.FC<Props> = ({ callback }) => {
isInvalid: false,
errorMsg: '',
},
captcha_code: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
const updateUser = userStore((state) => state.update);
const [imgCode, setImgCode] = useState<ImgCodeRes>({
captcha_id: '',
captcha_img: '',
verify: false,
});
const [showModal, setModalState] = useState(false);
const getImgCode = () => {
getRegisterCaptcha().then((res) => {
setImgCode(res);
});
};
const updateUser = userStore((state) => state.update);
const emailCaptcha = useCaptchaModal('email');
const handleChange = (params: FormDataType) => {
setFormData({ ...formData, ...params });
};
@ -86,6 +66,7 @@ const Index: React.FC<Props> = ({ callback }) => {
});
return bol;
};
const { data: tos } = useLegalTos();
const { data: privacy } = useLegalPrivacy();
const argumentClick = (evt: MouseEvent, type: 'tos' | 'privacy') => {
@ -117,25 +98,24 @@ const Index: React.FC<Props> = ({ callback }) => {
pass: formData.pass.value,
};
if (imgCode.verify) {
reqParams.captcha_code = formData.captcha_code.value;
reqParams.captcha_id = imgCode.captcha_id;
const captcha = emailCaptcha.getCaptcha();
if (captcha?.verify) {
reqParams.captcha_code = captcha.captcha_code;
reqParams.captcha_id = captcha.captcha_id;
}
register(reqParams)
.then((res) => {
emailCaptcha.close();
updateUser(res);
setModalState(false);
callback();
})
.catch((err) => {
if (err.isError) {
emailCaptcha.handleCaptchaError(err.list);
const data = handleFormError(err, formData);
if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
setModalState(false);
}
setFormData({ ...data });
}
getImgCode();
});
};
@ -145,15 +125,11 @@ const Index: React.FC<Props> = ({ callback }) => {
if (!checkValidated()) {
return;
}
if (imgCode.verify) {
setModalState(true);
return;
}
handleRegister();
emailCaptcha.check(() => {
handleRegister();
});
};
useEffect(() => {
getImgCode();
}, []);
return (
<>
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
@ -260,18 +236,6 @@ const Index: React.FC<Props> = ({ callback }) => {
Already have an account? <Link to="/users/login">Log in</Link>
</Trans>
</div>
<PicAuthCodeModal
visible={showModal}
data={{
captcha: formData.captcha_code,
imgCode,
}}
handleCaptcha={handleChange}
clickSubmit={handleRegister}
refreshImgCode={getImgCode}
onClose={() => setModalState(false)}
/>
</>
);
};

View File

@ -3,22 +3,15 @@ import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import type * as Type from '@/common/interface';
import { useToast } from '@/hooks';
import { getLoggedUserInfo, changeEmail, checkImgCode } from '@/services';
import { useToast, useCaptchaModal } from '@/hooks';
import { getLoggedUserInfo, changeEmail } from '@/services';
import { handleFormError } from '@/utils';
import { PicAuthCodeModal } from '@/components';
const Index: FC = () => {
const { t } = useTranslation('translation', {
keyPrefix: 'settings.account',
});
const [step, setStep] = useState(1);
const [showModal, setModalState] = useState(false);
const [imgCode, setImgCode] = useState<Type.ImgCodeRes>({
captcha_id: '',
captcha_img: '',
verify: false,
});
const [formData, setFormData] = useState<Type.FormDataType>({
e_mail: {
value: '',
@ -30,28 +23,17 @@ const Index: FC = () => {
isInvalid: false,
errorMsg: '',
},
captcha_code: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
const [userInfo, setUserInfo] = useState<Type.UserInfoRes>();
const toast = useToast();
const emailCaptcha = useCaptchaModal('edit_userinfo');
useEffect(() => {
getLoggedUserInfo().then((resp) => {
setUserInfo(resp);
});
}, []);
const getImgCode = () => {
checkImgCode({
action: 'e_mail',
}).then((res) => {
setImgCode(res);
});
};
const handleChange = (params: Type.FormDataType) => {
setFormData({ ...formData, ...params });
};
@ -95,11 +77,6 @@ const Index: FC = () => {
isInvalid: false,
errorMsg: '',
},
captcha_code: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
};
@ -112,14 +89,15 @@ const Index: FC = () => {
pass: formData.pass.value,
};
const imgCode = emailCaptcha.getCaptcha();
if (imgCode.verify) {
params.captcha_code = formData.captcha_code.value;
params.captcha_code = imgCode.captcha_code;
params.captcha_id = imgCode.captcha_id;
}
changeEmail(params)
.then(() => {
.then(async () => {
await emailCaptcha.close();
setStep(1);
setModalState(false);
toast.onShow({
msg: t('change_email_info'),
variant: 'warning',
@ -128,15 +106,10 @@ const Index: FC = () => {
})
.catch((err) => {
if (err.isError) {
emailCaptcha.handleCaptchaError(err.list);
const data = handleFormError(err, formData);
setFormData({ ...data });
if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
setModalState(false);
}
}
})
.finally(() => {
getImgCode();
});
};
@ -147,11 +120,9 @@ const Index: FC = () => {
return;
}
if (imgCode.verify) {
setModalState(true);
return;
}
postEmail();
emailCaptcha.check(() => {
postEmail();
});
};
return (
@ -174,7 +145,6 @@ const Index: FC = () => {
variant="outline-secondary"
onClick={() => {
setStep(2);
getImgCode();
}}>
{t('change_email_btn')}
</Button>
@ -240,18 +210,6 @@ const Index: FC = () => {
</div>
</Form>
)}
<PicAuthCodeModal
visible={showModal}
data={{
captcha: formData.captcha_code,
imgCode,
}}
handleCaptcha={handleChange}
clickSubmit={postEmail}
refreshImgCode={getImgCode}
onClose={() => setModalState(false)}
/>
</div>
);
};

View File

@ -4,12 +4,11 @@ import { useTranslation } from 'react-i18next';
import classname from 'classnames';
import { useToast } from '@/hooks';
import type { FormDataType, ImgCodeRes } from '@/common/interface';
import { modifyPassword, checkImgCode } from '@/services';
import { useToast, useCaptchaModal } from '@/hooks';
import type { FormDataType } from '@/common/interface';
import { modifyPassword } from '@/services';
import { handleFormError } from '@/utils';
import { loggedUserInfoStore } from '@/stores';
import { PicAuthCodeModal } from '@/components';
const Index: FC = () => {
const { t } = useTranslation('translation', {
@ -35,20 +34,8 @@ const Index: FC = () => {
errorMsg: '',
},
});
const [showModal, setModalState] = useState(false);
const [imgCode, setImgCode] = useState<ImgCodeRes>({
captcha_id: '',
captcha_img: '',
verify: false,
});
const getImgCode = () => {
checkImgCode({
action: 'modify_pass',
}).then((res) => {
setImgCode(res);
});
};
const infoCaptcha = useCaptchaModal('edit_userinfo');
const handleFormState = () => {
setFormState((pre) => !pre);
@ -128,13 +115,14 @@ const Index: FC = () => {
pass: formData.pass.value,
};
const imgCode = infoCaptcha.getCaptcha();
if (imgCode.verify) {
params.captcha_code = formData.captcha_code.value;
params.captcha_code = imgCode.captcha_code;
params.captcha_id = imgCode.captcha_id;
}
modifyPassword(params)
.then(() => {
setModalState(false);
.then(async () => {
await infoCaptcha.close();
toast.onShow({
msg: t('update_password', { keyPrefix: 'toast' }),
variant: 'success',
@ -143,15 +131,10 @@ const Index: FC = () => {
})
.catch((err) => {
if (err.isError) {
infoCaptcha.handleCaptchaError(err.list);
const data = handleFormError(err, formData);
if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
setModalState(false);
}
setFormData({ ...data });
}
})
.finally(() => {
getImgCode();
});
};
@ -162,11 +145,9 @@ const Index: FC = () => {
return;
}
if (imgCode.verify) {
setModalState(true);
return;
}
postModifyPass();
infoCaptcha.check(() => {
postModifyPass();
});
};
return (
@ -262,24 +243,11 @@ const Index: FC = () => {
type="submit"
onClick={() => {
handleFormState();
getImgCode();
}}>
{t('change_pass_btn')}
</Button>
</>
)}
<PicAuthCodeModal
visible={showModal}
data={{
captcha: formData.captcha_code,
imgCode,
}}
handleCaptcha={handleChange}
clickSubmit={postModifyPass}
refreshImgCode={getImgCode}
onClose={() => setModalState(false)}
/>
</div>
);
};

View File

@ -61,10 +61,15 @@ export const getInviteUser = (questionId: string) => {
});
};
export const putInviteUser = (questionId: string, users: string[]) => {
export const putInviteUser = (
questionId: string,
users: string[],
imgCode: Type.ImgCodeReq = {},
) => {
const apiUrl = '/answer/api/v1/question/invite';
return request.put(apiUrl, {
id: questionId,
invite_user: users,
...imgCode,
});
};

View File

@ -1,20 +1,10 @@
import useSWR from 'swr';
import qs from 'qs';
import request from '@/utils/request';
import type * as Type from '@/common/interface';
export const useSearch = (params?: Type.SearchParams) => {
export const getSearchResult = (params?: Type.SearchParams) => {
const apiUrl = '/answer/api/v1/search';
const queryParams = qs.stringify(params, { skipNulls: true });
const { data, error, mutate } = useSWR<Type.SearchRes, Error>(
params?.q ? `${apiUrl}?${queryParams}` : null,
request.instance.get,
);
return {
data,
isLoading: !data && !error,
error,
mutate,
};
return request.get<Type.SearchRes>(apiUrl, {
params,
});
};

View File

@ -60,9 +60,10 @@ export const updateComment = (params) => {
return request.put('/answer/api/v1/comment', params);
};
export const deleteComment = (id) => {
export const deleteComment = (id, imgCode: Type.ImgCodeReq = {}) => {
return request.delete('/answer/api/v1/comment', {
comment_id: id,
...imgCode,
});
};
@ -102,19 +103,10 @@ export const register = (params: Type.RegisterReqParams) => {
return request.post<any>('/answer/api/v1/user/register/email', params);
};
export const getRegisterCaptcha = () => {
const apiUrl = '/answer/api/v1/user/register/captcha';
return request.get(apiUrl);
};
export const logout = () => {
return request.get('/answer/api/v1/user/logout');
};
export const verifyEmail = (code: string) => {
return request.get(`/answer/api/v1/email/verify?code=${code}`);
};
export const resendEmail = (params?: Type.ImgCodeReq) => {
params = qs.parse(
qs.stringify(params, {
@ -134,19 +126,19 @@ export const getLoggedUserInfo = (config = { passingError: false }) => {
return request.get<Type.UserInfoRes>('/answer/api/v1/user/info', config);
};
export const modifyPassword = (params: Type.ModifyPasswordReq) => {
return request.put('/answer/api/v1/user/password', params);
};
export const modifyUserInfo = (params: Type.ModifyUserReq) => {
return request.put('/answer/api/v1/user/info', params);
};
export const modifyPassword = (params: Type.ModifyPasswordReq) => {
return request.put('/answer/api/v1/user/password', params);
};
export const resetPassword = (params: Type.PasswordResetReq) => {
return request.post('/answer/api/v1/user/password/reset', params);
};
export const replacementPassword = (params: { code: string; pass: string }) => {
export const replacementPassword = (params: Type.PasswordReplaceReq) => {
return request.post('/answer/api/v1/user/password/replacement', params);
};
@ -154,10 +146,13 @@ export const activateAccount = (code: string) => {
return request.post(`/answer/api/v1/user/email/verification`, { code });
};
export const checkImgCode = (params: Type.CheckImgReq) => {
return request.get<Type.ImgCodeRes>(
`/answer/api/v1/user/action/record?${qs.stringify(params)}`,
);
export const checkImgCode = (k: Type.CaptchaKey) => {
const apiUrl = `/answer/api/v1/user/action/record`;
return request.get<Type.ImgCodeRes>(apiUrl, {
params: {
action: k,
},
});
};
export const setNotice = (params: Type.SetNoticeReq) => {
@ -189,7 +184,7 @@ export const bookmark = (params: { group_id: string; object_id: string }) => {
};
export const postVote = (
params: { object_id: string; is_cancel: boolean },
params: { object_id: string; is_cancel: boolean } & Type.ImgCodeReq,
type: 'down' | 'up',
) => {
return request.post(`/answer/api/v1/vote/${type}`, params);
@ -224,20 +219,30 @@ export const reportList = ({
return request.get(`${api}?object_type=${type}&action=${action}`);
};
export const postReport = (params: {
source: Type.ReportType;
content: string;
object_id: string;
report_type: number;
}) => {
export const postReport = (
params: {
source: Type.ReportType;
content: string;
object_id: string;
report_type: number;
} & Type.ImgCodeReq,
) => {
return request.post('/answer/api/v1/report', params);
};
export const deleteQuestion = (params: { id: string }) => {
export const deleteQuestion = (params: {
id: string;
captcha_code?: string;
captcha_id?: string;
}) => {
return request.delete('/answer/api/v1/question', params);
};
export const deleteAnswer = (params: { id: string }) => {
export const deleteAnswer = (params: {
id: string;
captcha_code?: string;
captcha_id?: string;
}) => {
return request.delete('/answer/api/v1/answer', params);
};

View File

@ -2,8 +2,7 @@ import i18next from 'i18next';
import pattern from '@/common/pattern';
import { USER_AGENT_NAMES } from '@/common/constants';
const Diff = require('diff');
import type * as Type from '@/common/interface';
function thousandthDivision(num) {
const reg = /\d{1,3}(?=(\d{3})+$)/g;
@ -114,7 +113,7 @@ function escapeRemove(str: string) {
}
function handleFormError(
error: { list: Array<{ error_field: string; error_msg: string }> },
error: { list: Type.FieldError[] },
data: any,
keymap?: Array<{ from: string; to: string }>,
) {
@ -148,6 +147,8 @@ function escapeHtml(str: string) {
return str.replace(/[&<>"'`]/g, (tag) => tagsToReplace[tag] || tag);
}
const Diff = require('diff');
function diffText(newText: string, oldText?: string): string {
if (!newText) {
return '';

View File

@ -94,36 +94,42 @@ export interface NavigateConfig {
}
const navigate = (to: string | number, config: NavigateConfig = {}) => {
let { handler = 'href' } = config;
if (to && typeof to === 'string') {
if (equalToCurrentHref(to)) {
return;
}
/**
* 1. Blocking redirection of two login pages
* 2. Auto storage login redirect
* Note: The or judgement cannot be missing here, both jumps will be used
*/
if (to === RouteAlias.login || to === getLoginUrl()) {
storageLoginRedirect();
/**
* Note: Synchronised navigation can result in asynchronous actions such as page animations and state modifications not being completed.
*/
setTimeout(() => {
if (to && typeof to === 'string') {
if (equalToCurrentHref(to)) {
return;
}
/**
* 1. Blocking redirection of two login pages
* 2. Auto storage login redirect
* Note: The or judgement cannot be missing here, both jumps will be used
*/
if (to === RouteAlias.login || to === getLoginUrl()) {
storageLoginRedirect();
}
if (!isRoutableLink(to) && handler !== 'href' && handler !== 'replace') {
handler = 'href';
}
if (handler === 'href' && config.options?.replace) {
handler = 'replace';
}
if (handler === 'href') {
window.location.href = to;
} else if (handler === 'replace') {
window.location.replace(to);
} else if (typeof handler === 'function') {
handler(to, config.options);
}
}
if (!isRoutableLink(to) && handler !== 'href' && handler !== 'replace') {
handler = 'href';
if (typeof to === 'number' && typeof handler === 'function') {
handler(to);
}
if (handler === 'href' && config.options?.replace) {
handler = 'replace';
}
if (handler === 'href') {
window.location.href = to;
} else if (handler === 'replace') {
window.location.replace(to);
} else if (typeof handler === 'function') {
handler(to, config.options);
}
}
if (typeof to === 'number' && typeof handler === 'function') {
handler(to);
}
});
};
/**

View File

@ -61,6 +61,7 @@ class Request {
config: errConfig,
} = error.response || {};
const { data = {}, msg = '' } = errBody || {};
const errorObject: {
code: any;
msg: string;
@ -74,6 +75,7 @@ class Request {
msg,
data,
};
if (status === 400) {
if (data?.err_type && errConfig?.passingError) {
return Promise.reject(errorObject);
@ -127,6 +129,7 @@ class Request {
floppyNavigation.navigateToLogin();
return Promise.reject(false);
}
if (status === 403) {
// Permission interception
if (data?.type === 'url_expired') {
@ -173,6 +176,7 @@ class Request {
errorCodeStore.getState().update('404');
return Promise.reject(false);
}
if (status >= 500) {
if (isIgnoredPath(IGNORE_PATH_LIST)) {
return Promise.reject(false);