feat: Implementing `userCenter` related functions

This commit is contained in:
haitaoo 2023-03-28 16:19:53 +08:00
parent f4212e2cd0
commit e8e5bdf181
18 changed files with 222 additions and 64 deletions

View File

@ -20,7 +20,7 @@ import {
import classnames from 'classnames';
import { floppyNavigation } from '@/utils';
import { floppyNavigation, userCenter } from '@/utils';
import {
loggedUserInfoStore,
siteInfoStore,
@ -29,6 +29,7 @@ import {
themeSettingStore,
} from '@/stores';
import { logout, useQueryNotificationStatus } from '@/services';
import { RouteAlias } from '@/router/alias';
import NavItems from './components/NavItems';
@ -46,6 +47,9 @@ const Header: FC = () => {
const brandingInfo = brandingStore((state) => state.branding);
const loginSetting = loginSettingStore((state) => state.login);
const { data: redDot } = useQueryNotificationStatus();
/**
* Automatically append `tag` information when creating a question
*/
const tagMatch = useMatch('/tags/:slugName');
let askUrl = '/questions/ask';
if (tagMatch && tagMatch.params.slugName) {
@ -70,16 +74,14 @@ const Header: FC = () => {
window.location.replace(window.location.href);
};
const onLoginClick = (evt) => {
if (location.pathname === '/users/login') {
if (location.pathname === RouteAlias.login) {
evt.preventDefault();
window.location.reload();
return;
}
if (floppyNavigation.shouldProcessLinkClick(evt)) {
evt.preventDefault();
floppyNavigation.navigateToLogin((loginPath) => {
navigate(loginPath, { replace: true });
});
floppyNavigation.navigateToLogin();
}
};
@ -152,7 +154,7 @@ const Header: FC = () => {
'link-primary': navbarStyle !== 'theme-colored',
})}
onClick={onLoginClick}
href="/users/login">
href={userCenter.getLoginUrl()}>
{t('btns.login')}
</Button>
{loginSetting.allow_new_registrations && (
@ -160,7 +162,7 @@ const Header: FC = () => {
variant={
navbarStyle === 'theme-colored' ? 'light' : 'primary'
}
href="/users/register">
href={userCenter.getSignUpUrl()}>
{t('btns.signup')}
</Button>
)}
@ -240,7 +242,7 @@ const Header: FC = () => {
'link-primary': navbarStyle !== 'theme-colored',
})}
onClick={onLoginClick}
href="/users/login">
href={userCenter.getLoginUrl()}>
{t('btns.login')}
</Button>
{loginSetting.allow_new_registrations && (
@ -248,7 +250,7 @@ const Header: FC = () => {
variant={
navbarStyle === 'theme-colored' ? 'light' : 'primary'
}
href="/users/register">
href={userCenter.getSignUpUrl()}>
{t('btns.signup')}
</Button>
)}

View File

@ -11,8 +11,11 @@ const Index = () => {
const loginRedirect = () => {
const redirect = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
floppyNavigation.navigate(redirect, () => {
navigate(redirect, { replace: true });
floppyNavigation.navigate(redirect, {
handler: navigate,
options: {
replace: true,
},
});
};

View File

@ -8,6 +8,7 @@ import { FollowingTags, QuestionList, HotQuestions } from '@/components';
import { siteInfoStore, loggedUserInfoStore } from '@/stores';
import { useQuestionList } from '@/services';
import * as Type from '@/common/interface';
import { userCenter } from '@/utils';
const Questions: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'question' });
@ -43,7 +44,7 @@ const Questions: FC = () => {
/>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
{!loggedUser.access_token && (
{!loggedUser.username && (
<div className="card mb-4">
<div className="card-body">
<h5 className="card-title">
@ -52,10 +53,12 @@ const Questions: FC = () => {
})}
</h5>
<p className="card-text">{siteInfo.description}</p>
<Link to="/users/login" className="btn btn-primary">
<Link to={userCenter.getLoginUrl()} className="btn btn-primary">
{t('login', { keyPrefix: 'btns' })}
</Link>
<Link to="/users/register" className="btn btn-link ms-2">
<Link
to={userCenter.getSignUpUrl()}
className="btn btn-link ms-2">
{t('signup', { keyPrefix: 'btns' })}
</Link>
</div>

View File

@ -94,8 +94,9 @@ const Index: React.FC = () => {
const handleLoginRedirect = () => {
const redirect = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
floppyNavigation.navigate(redirect, () => {
navigate(redirect, { replace: true });
floppyNavigation.navigate(redirect, {
handler: navigate,
options: { replace: true },
});
};
@ -168,6 +169,9 @@ const Index: React.FC = () => {
usePageTags({
title: t('login', { keyPrefix: 'page_title' }),
});
if (!guard.loginAgent().ok) {
return null;
}
return (
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
<WelcomeTitle />

View File

@ -12,7 +12,7 @@ import {
useLegalTos,
useLegalPrivacy,
} from '@/services';
import userStore from '@/stores/loggedUserInfoStore';
import userStore from '@/stores/loggedUserInfo';
import { handleFormError } from '@/utils';
interface Props {

View File

@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { Unactivate, WelcomeTitle } from '@/components';
import { PluginOauth } from '@/plugins';
import { guard } from '@/utils';
import SignUpForm from './components/SignUpForm';
@ -17,6 +18,9 @@ const Index: React.FC = () => {
usePageTags({
title: t('sign_up', { keyPrefix: 'page_title' }),
});
if (!guard.singUpAgent().ok) {
return null;
}
return (
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
<WelcomeTitle />

View File

@ -21,8 +21,9 @@ const Index: FC<{
const gr = onEnter();
const redirectUrl = gr.redirect;
if (redirectUrl) {
floppyNavigation.navigate(redirectUrl, () => {
navigate(redirectUrl, { replace: true });
floppyNavigation.navigate(redirectUrl, {
handler: navigate,
options: { replace: true },
});
}
}

View File

@ -1,7 +1,7 @@
export const RouteAlias = {
home: '/',
login: '/users/login',
register: '/users/register',
signUp: '/users/register',
activation: '/users/login?status=inactive',
activationFailed: '/users/account-activation/failed',
suspended: '/users/account-suspended',

View File

@ -158,8 +158,13 @@ const routes: RouteNode[] = [
guard: () => {
const notLogged = guard.notLogged();
if (notLogged.ok) {
const la = guard.loginAgent();
if (!la.ok) {
return la;
}
return notLogged;
}
return guard.notActivated();
},
},
@ -171,7 +176,14 @@ const routes: RouteNode[] = [
if (!allowNew.ok) {
return allowNew;
}
return guard.notLogged();
const notLogged = guard.notLogged();
if (notLogged.ok) {
const sa = guard.singUpAgent();
if (!sa.ok) {
return sa;
}
}
return notLogged;
},
},
{

View File

@ -0,0 +1,23 @@
import request from '@/utils/request';
export interface UcAgentControl {
name: string;
label: string;
url: string;
}
export interface UcAgent {
enabled: boolean;
agent_info: {
name: string;
icon: string;
url: string;
login_redirect_url: string;
sign_up_redirect_url: string;
control_center: UcAgentControl[];
};
}
export const getUcAgent = () => {
const apiUrl = `/answer/api/v1/user-center/agent`;
return request.get<UcAgent>(apiUrl);
};

View File

@ -1,8 +1,8 @@
import loginSettingStore from '@/stores/loginSetting';
import seoSettingStore from '@/stores/seoSetting';
import loginSettingStore from './loginSetting';
import seoSettingStore from './seoSetting';
import userCenterStore from './userCenter';
import toastStore from './toast';
import loggedUserInfoStore from './loggedUserInfoStore';
import loggedUserInfoStore from './loggedUserInfo';
import siteInfoStore from './siteInfo';
import interfaceStore from './interface';
import brandingStore from './branding';
@ -25,4 +25,5 @@ export {
seoSettingStore,
loginToContinueStore,
errorCode,
userCenterStore,
};

View File

@ -31,7 +31,7 @@ const initUser: UserInfoRes = {
role_id: 1,
};
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
const loggedUserInfo = create<UserInfoStore>((set) => ({
user: initUser,
update: (params) => {
if (!params.language) {
@ -51,4 +51,4 @@ const loggedUserInfoStore = create<UserInfoStore>((set) => ({
}),
}));
export default loggedUserInfoStore;
export default loggedUserInfo;

View File

@ -0,0 +1,21 @@
import create from 'zustand';
import type { UcAgent } from '@/services/user-center';
interface UserCenterStore {
agent?: UcAgent;
update: (uca: UcAgent) => void;
}
const store = create<UserCenterStore>((set) => ({
agent: undefined,
update: (uca: UcAgent) => {
if (uca) {
set({
agent: uca,
});
}
},
}));
export default store;

View File

@ -1,6 +1,9 @@
import type { NavigateFunction } from 'react-router-dom';
import { RouteAlias } from '@/router/alias';
import Storage from '@/utils/storage';
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
import { getLoginUrl } from '@/utils/userCenter';
const differentCurrent = (target: string, base?: string) => {
base ||= window.location.origin;
@ -10,37 +13,70 @@ const differentCurrent = (target: string, base?: string) => {
const storageLoginRedirect = () => {
const { pathname } = window.location;
if (pathname !== RouteAlias.login && pathname !== RouteAlias.register) {
if (pathname !== RouteAlias.login && pathname !== RouteAlias.signUp) {
const loc = window.location;
const redirectUrl = loc.href.replace(loc.origin, '');
Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl);
}
};
/**
* Determining whether an url is an external link
*/
const isExternalLink = (url = '') => {
let ret = false;
try {
const urlObject = new URL(url, document.baseURI);
if (urlObject && urlObject.origin !== window.location.origin) {
ret = true;
}
// eslint-disable-next-line no-empty
} catch (ex) {}
return ret;
};
/**
* only navigate if not same as current url
* @param pathname
* @param callback
*/
const navigate = (pathname: string, callback: Function) => {
if (pathname === RouteAlias.login) {
storageLoginRedirect();
type NavigateHandler = 'href' | 'replace' | NavigateFunction;
interface NavigateConfig {
handler: NavigateHandler;
options?: any;
}
const navigate = (
to: string | number,
config: NavigateConfig = { handler: 'href' },
) => {
let { handler } = config;
if (to && typeof to === 'string') {
if (!differentCurrent(to)) {
return;
}
if (to === RouteAlias.login || to === getLoginUrl()) {
storageLoginRedirect();
}
if (isExternalLink(to)) {
handler = 'href';
}
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 (differentCurrent(pathname)) {
callback();
if (typeof to === 'number' && typeof handler === 'function') {
handler(to);
}
};
/**
* auto navigate to login page with redirect info
*/
const navigateToLogin = (callback?: Function) => {
navigate(RouteAlias.login, () => {
if (callback) {
callback(RouteAlias.login);
} else {
window.location.replace(RouteAlias.login);
}
});
const navigateToLogin = (config?: NavigateConfig) => {
const loginUrl = getLoginUrl();
navigate(loginUrl, config);
};
/**

View File

@ -16,6 +16,7 @@ import { LOGGED_USER_STORAGE_KEY } from '@/common/constants';
import Storage from './storage';
import { setupAppLanguage, setupAppTimeZone } from './localize';
import { floppyNavigation } from './floppyNavigation';
import { pullUcAgent, getLoginUrl, getSignUpUrl } from './userCenter';
type TLoginState = {
isLogged: boolean;
@ -79,20 +80,20 @@ export const isIgnoredPath = (ignoredPath: string | string[]) => {
return !!matchingPath;
};
let pullLock = false;
let dedupeTimestamp = 0;
let pluLock = false;
let pluTimestamp = 0;
export const pullLoggedUser = async (forceRePull = false) => {
// only pull once if not force re-pull
if (pullLock && !forceRePull) {
if (pluLock && !forceRePull) {
return;
}
// dedupe pull requests in this time span in 10 seconds
if (Date.now() - dedupeTimestamp < 1000 * 10) {
if (Date.now() - pluTimestamp < 1000 * 10) {
return;
}
dedupeTimestamp = Date.now();
pluTimestamp = Date.now();
const loggedUserInfo = await getLoggedUserInfo().catch((ex) => {
dedupeTimestamp = 0;
pluTimestamp = 0;
if (!deriveLoginState().isLogged) {
// load fallback userInfo from local storage
const storageLoggedUserInfo = Storage.get(LOGGED_USER_STORAGE_KEY);
@ -103,7 +104,7 @@ export const pullLoggedUser = async (forceRePull = false) => {
console.error(ex);
});
if (loggedUserInfo) {
pullLock = true;
pluLock = true;
loggedUserInfoStore.getState().update(loggedUserInfo);
}
};
@ -198,6 +199,26 @@ export const allowNewRegistration = () => {
return gr;
};
export const loginAgent = () => {
const gr: TGuardResult = { ok: true };
const loginUrl = getLoginUrl();
if (loginUrl !== RouteAlias.login) {
gr.ok = false;
gr.redirect = loginUrl;
}
return gr;
};
export const singUpAgent = () => {
const gr: TGuardResult = { ok: true };
const signUpUrl = getSignUpUrl();
if (signUpUrl !== RouteAlias.signUp) {
gr.ok = false;
gr.redirect = signUpUrl;
}
return gr;
};
export const shouldLoginRequired = () => {
const gr: TGuardResult = { ok: true };
const loginSetting = loginSettingStore.getState().login;
@ -211,7 +232,7 @@ export const shouldLoginRequired = () => {
if (
isIgnoredPath([
RouteAlias.login,
RouteAlias.register,
RouteAlias.signUp,
'/users/account-recovery',
'users/change-email',
'users/password-reset',
@ -246,12 +267,10 @@ export const tryNormalLogged = (canNavigate: boolean = false) => {
return false;
}
if (us.isNotActivated) {
floppyNavigation.navigate(RouteAlias.activation, () => {
window.location.href = RouteAlias.activation;
});
floppyNavigation.navigate(RouteAlias.activation);
} else if (us.isForbidden) {
floppyNavigation.navigate(RouteAlias.suspended, () => {
window.location.replace(RouteAlias.suspended);
floppyNavigation.navigate(RouteAlias.suspended, {
handler: 'replace',
});
}
@ -296,7 +315,11 @@ export const setupApp = async () => {
* 2. must pre init app settings for app render
*/
// TODO: optimize `initAppSettingsStore` by server render
await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]);
await Promise.allSettled([
pullLoggedUser(),
pullUcAgent(),
initAppSettingsStore(),
]);
setupAppLanguage();
setupAppTimeZone();
appInitialized = true;

View File

@ -6,5 +6,6 @@ export { default as SaveDraft } from './saveDraft';
export * from './common';
export * from './color';
export * as userCenter from './userCenter';
export * as localize from './localize';
export * as guard from './guard';

View File

@ -117,22 +117,20 @@ class Request {
errorCode.getState().reset();
if (data?.type === 'url_expired') {
// url expired
floppyNavigation.navigate(RouteAlias.activationFailed, () => {
window.location.replace(RouteAlias.activationFailed);
floppyNavigation.navigate(RouteAlias.activationFailed, {
handler: 'replace',
});
return Promise.reject(false);
}
if (data?.type === 'inactive') {
// inactivated
floppyNavigation.navigate(RouteAlias.activation, () => {
window.location.href = RouteAlias.activation;
});
floppyNavigation.navigate(RouteAlias.activation);
return Promise.reject(false);
}
if (data?.type === 'suspended') {
floppyNavigation.navigate(RouteAlias.suspended, () => {
window.location.replace(RouteAlias.suspended);
floppyNavigation.navigate(RouteAlias.suspended, {
handler: 'replace',
});
return Promise.reject(false);
}

View File

@ -0,0 +1,26 @@
import { RouteAlias } from '@/router/alias';
import { userCenterStore } from '@/stores';
import { getUcAgent, UcAgent } from '@/services/user-center';
export const pullUcAgent = async () => {
const uca = await getUcAgent();
userCenterStore.getState().update(uca);
};
export const getLoginUrl = (uca?: UcAgent) => {
let ret = RouteAlias.login;
uca ||= userCenterStore.getState().agent;
if (uca?.enabled && uca?.agent_info?.login_redirect_url) {
ret = uca.agent_info.login_redirect_url;
}
return ret;
};
export const getSignUpUrl = (uca?: UcAgent) => {
let ret = RouteAlias.signUp;
uca ||= userCenterStore.getState().agent;
if (uca?.enabled && uca?.agent_info?.sign_up_redirect_url) {
ret = uca.agent_info.sign_up_redirect_url;
}
return ret;
};