chore: add oauth pages

This commit is contained in:
shuai 2023-01-10 16:49:50 +08:00
parent d3a803d8a6
commit 0a1f1b1372
16 changed files with 376 additions and 3 deletions

View File

@ -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

View File

@ -515,3 +515,9 @@ export interface User {
display_name: string;
avatar: string;
}
export interface OauthBindEmailReq {
binding_key: string;
email: string;
must: boolean;
}

View File

@ -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();
};

View File

@ -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);

View File

@ -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 };

View File

@ -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,
};

View File

@ -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;

View File

@ -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>

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -0,0 +1,5 @@
import ModifyEmail from './ModifyEmail';
import ModifyPassword from './ModifyPass';
import MyLogins from './MyLogins';
export { ModifyEmail, ModifyPassword, MyLogins };

View File

@ -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 />
</>
);
};

View File

@ -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',

View File

@ -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);
};

View File

@ -9,3 +9,4 @@ export * from './legal';
export * from './timeline';
export * from './revision';
export * from './user';
export * from './Oauth';