mirror of https://gitee.com/answerdev/answer.git
chore: add oauth pages
This commit is contained in:
parent
d3a803d8a6
commit
0a1f1b1372
|
@ -281,6 +281,7 @@ ui:
|
|||
upgrade: Answer Upgrade
|
||||
maintenance: Website Maintenance
|
||||
users: Users
|
||||
oauth_callback: Processing
|
||||
notifications:
|
||||
title: Notifications
|
||||
inbox: Inbox
|
||||
|
@ -647,6 +648,18 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth_bind_email:
|
||||
page_title: Welcome to Answer
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
email:
|
||||
label: Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
modal_title: Email already existes.
|
||||
modal_content: This email address already registered. Are you sure you want to connect to the existing account?
|
||||
modal_cancel: Change email
|
||||
modal_confirm: Connect to the existing account
|
||||
password_reset:
|
||||
page_title: Password Reset
|
||||
btn_name: Reset my password
|
||||
|
|
|
@ -515,3 +515,9 @@ export interface User {
|
|||
display_name: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export interface OauthBindEmailReq {
|
||||
binding_key: string;
|
||||
email: string;
|
||||
must: boolean;
|
||||
}
|
||||
|
|
|
@ -18,12 +18,16 @@ const Index = ({
|
|||
title = '',
|
||||
confirmText = '',
|
||||
content,
|
||||
onCancel: onClose,
|
||||
onConfirm,
|
||||
cancelBtnVariant = 'link',
|
||||
confirmBtnVariant = 'primary',
|
||||
...props
|
||||
}: Config) => {
|
||||
const onCancel = () => {
|
||||
if (typeof onClose === 'function') {
|
||||
onClose();
|
||||
}
|
||||
render({ visible: false });
|
||||
div.remove();
|
||||
};
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { memo, FC } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Icon } from '@/components';
|
||||
|
||||
interface Props {
|
||||
// data: any[]; // should use oauth plugin schemes
|
||||
className?: string;
|
||||
}
|
||||
const Index: FC<Props> = ({ className }) => {
|
||||
return (
|
||||
<div className={classnames('d-grid gap-2', className)}>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
href="https://github.com/login/oauth/authorize?client_id=8cb9d4760cfd71c24de9&edirect_uri=http://10.0.20.88:8080/answer/api/v1/connector/redirect/github">
|
||||
<Icon name="github" className="me-2" />
|
||||
<span>Connect with Github</span>
|
||||
</Button>
|
||||
|
||||
<Button variant="outline-secondary">
|
||||
<Icon name="twitter" className="me-2" />
|
||||
<span>Connect with Google</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -32,6 +32,7 @@ import CustomizeTheme from './CustomizeTheme';
|
|||
import PageTags from './PageTags';
|
||||
import QuestionListLoader from './QuestionListLoader';
|
||||
import TagsLoader from './TagsLoader';
|
||||
import OauthButtons from './OauthButtons';
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
|
@ -70,5 +71,6 @@ export {
|
|||
PageTags,
|
||||
QuestionListLoader,
|
||||
TagsLoader,
|
||||
OauthButtons,
|
||||
};
|
||||
export type { EditorRef, JSONSchema, UISchema };
|
||||
|
|
|
@ -8,6 +8,7 @@ import useChangeUserRoleModal from './useChangeUserRoleModal';
|
|||
import useUserModal from './useUserModal';
|
||||
import useChangePasswordModal from './useChangePasswordModal';
|
||||
import usePageTags from './usePageTags';
|
||||
import useLoginRedirect from './useLoginRedirect';
|
||||
|
||||
export {
|
||||
useTagModal,
|
||||
|
@ -20,4 +21,5 @@ export {
|
|||
useUserModal,
|
||||
useChangePasswordModal,
|
||||
usePageTags,
|
||||
useLoginRedirect,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import Storage from '@/utils/storage';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||
|
||||
const Index = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const loginRedirect = () => {
|
||||
const redirect = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
|
||||
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
|
||||
debugger;
|
||||
floppyNavigation.navigate(redirect, () => {
|
||||
navigate(redirect, { replace: true });
|
||||
});
|
||||
};
|
||||
|
||||
return { loginRedirect };
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -11,7 +11,7 @@ import type {
|
|||
ImgCodeRes,
|
||||
FormDataType,
|
||||
} from '@/common/interface';
|
||||
import { Unactivate } from '@/components';
|
||||
import { Unactivate, OauthButtons } from '@/components';
|
||||
import { loggedUserInfoStore, loginSettingStore } from '@/stores';
|
||||
import { guard, floppyNavigation, handleFormError } from '@/utils';
|
||||
import { login, checkImgCode } from '@/services';
|
||||
|
@ -173,6 +173,7 @@ const Index: React.FC = () => {
|
|||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
{step === 1 && (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<OauthButtons className="mb-5" />
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<Form.Label>{t('email.label')}</Form.Label>
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
import { FC, memo, useState, useEffect } from 'react';
|
||||
import { Container, Col, Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Modal } from '@/components';
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { oAuthBindEmail, getLoggedUserInfo } from '@/services';
|
||||
import Storage from '@/utils/storage';
|
||||
import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'oauth_bind_email',
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setUrlSearchParams] = useSearchParams();
|
||||
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||
const binding_key = searchParams.get('binding_key') || '';
|
||||
const [showResult, setShowResult] = useState(false);
|
||||
|
||||
usePageTags({
|
||||
title: t('confirm_email', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
email: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleChange = (params: FormDataType) => {
|
||||
setFormData({ ...formData, ...params });
|
||||
};
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
|
||||
if (!formData.email.value) {
|
||||
bol = false;
|
||||
formData.email = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('email.msg.empty'),
|
||||
};
|
||||
}
|
||||
setFormData({
|
||||
...formData,
|
||||
});
|
||||
return bol;
|
||||
};
|
||||
|
||||
const getUserInfo = (token) => {
|
||||
Storage.set(LOGGED_TOKEN_STORAGE_KEY, token);
|
||||
getLoggedUserInfo().then((user) => {
|
||||
updateUser(user);
|
||||
setTimeout(() => {
|
||||
navigate('/users/login?status=inactive', { replace: true });
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
const connectConfirm = () => {
|
||||
Modal.confirm({
|
||||
title: t('modal_title'),
|
||||
content: t('modal_content'),
|
||||
cancelText: t('modal_cancel'),
|
||||
confirmText: t('modal_confirm'),
|
||||
onConfirm: () => {
|
||||
// send activation email
|
||||
oAuthBindEmail({
|
||||
binding_key,
|
||||
email: formData.email.value,
|
||||
must: true,
|
||||
}).then((result) => {
|
||||
debugger;
|
||||
if (result.access_token) {
|
||||
getUserInfo(result.access_token);
|
||||
} else {
|
||||
searchParams.delete('binding_key');
|
||||
setUrlSearchParams('');
|
||||
setShowResult(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel: () => {
|
||||
setFormData({
|
||||
email: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!checkValidated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (binding_key) {
|
||||
oAuthBindEmail({
|
||||
binding_key,
|
||||
email: formData.email.value,
|
||||
must: false,
|
||||
})
|
||||
.then((res) => {
|
||||
debugger;
|
||||
if (res.email_exist_and_must_be_confirmed) {
|
||||
connectConfirm();
|
||||
}
|
||||
if (res.access_token) {
|
||||
getUserInfo(res.access_token);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!binding_key) {
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
{showResult ? (
|
||||
<Col md={6} className="mx-auto text-center">
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="inactive.first"
|
||||
values={{ mail: formData.email.value }}
|
||||
components={{ bold: <strong /> }}
|
||||
/>
|
||||
</p>
|
||||
<p>{t('info', { keyPrefix: 'inactive' })}</p>
|
||||
</Col>
|
||||
) : (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<div className="text-center mb-5">{t('subtitle')}</div>
|
||||
<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.email.value}
|
||||
isInvalid={formData.email.isInvalid}
|
||||
onChange={(e) => {
|
||||
handleChange({
|
||||
email: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.email.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<div className="d-grid mb-3">
|
||||
<Button variant="primary" type="submit">
|
||||
{t('btn_update')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Col>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -0,0 +1,39 @@
|
|||
import { FC, memo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { usePageTags, useLoginRedirect } from '@/hooks';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { getLoggedUserInfo } from '@/services';
|
||||
import Storage from '@/utils/storage';
|
||||
import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||
const [searchParams] = useSearchParams();
|
||||
const { loginRedirect } = useLoginRedirect();
|
||||
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const token = searchParams.get('access_token');
|
||||
|
||||
if (token) {
|
||||
Storage.set(LOGGED_TOKEN_STORAGE_KEY, token);
|
||||
getLoggedUserInfo().then((res) => {
|
||||
updateUser(res);
|
||||
setTimeout(() => {
|
||||
loginRedirect();
|
||||
}, 0);
|
||||
});
|
||||
} else {
|
||||
navigate('/', { replace: true });
|
||||
}
|
||||
}, []);
|
||||
usePageTags({
|
||||
title: t('oauth_callback'),
|
||||
});
|
||||
return null;
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -0,0 +1,43 @@
|
|||
import { memo } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
import { Icon, Modal } from '@/components';
|
||||
|
||||
const Index = () => {
|
||||
const deleteLogins = (type) => {
|
||||
Modal.confirm({
|
||||
title: 'Remove Login',
|
||||
content: 'Are you sure you want to delete this logins?',
|
||||
confirmBtnVariant: 'danger',
|
||||
confirmText: 'Remove',
|
||||
onConfirm: () => {
|
||||
console.log('delete login by: ', type);
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<div className="form-label">My Logins</div>
|
||||
<small className="form-text mt-0">
|
||||
Log in or sign up on this site using these accounts.
|
||||
</small>
|
||||
|
||||
<div className="mt-3">
|
||||
<Button variant="outline-secondary" className="d-block mb-2">
|
||||
<Icon name="google" className="me-2" />
|
||||
<span>Connect with Google</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
className="mb-2"
|
||||
onClick={() => deleteLogins('github')}>
|
||||
<Icon name="github" className="me-2" />
|
||||
<span>Remove GitHub</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -0,0 +1,5 @@
|
|||
import ModifyEmail from './ModifyEmail';
|
||||
import ModifyPassword from './ModifyPass';
|
||||
import MyLogins from './MyLogins';
|
||||
|
||||
export { ModifyEmail, ModifyPassword, MyLogins };
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModifyEmail from './components/ModifyEmail';
|
||||
import ModifyPassword from './components/ModifyPass';
|
||||
import { ModifyEmail, ModifyPassword, MyLogins } from './components';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
|
@ -13,6 +12,7 @@ const Index = () => {
|
|||
<h3 className="mb-4">{t('heading')}</h3>
|
||||
<ModifyEmail />
|
||||
<ModifyPassword />
|
||||
<MyLogins />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -203,6 +203,14 @@ const routes: RouteNode[] = [
|
|||
return guard.forbidden();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/users/confirm-email',
|
||||
page: 'pages/Users/OauthBindEmail',
|
||||
},
|
||||
{
|
||||
path: '/users/oauth',
|
||||
page: 'pages/Users/OauthCallback',
|
||||
},
|
||||
{
|
||||
path: '/posts/:qid/timeline',
|
||||
page: 'pages/Timeline',
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import request from '@/utils/request';
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
export const oAuthBindEmail = (data: Type.OauthBindEmailReq) => {
|
||||
return request.post('/answer/api/v1/connector/binding/email', data);
|
||||
};
|
|
@ -9,3 +9,4 @@ export * from './legal';
|
|||
export * from './timeline';
|
||||
export * from './revision';
|
||||
export * from './user';
|
||||
export * from './Oauth';
|
||||
|
|
Loading…
Reference in New Issue