blob: e033f795aaa9796abf21e6648452980787d97eb5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getLoggedUserInfo, getAppSettings } from '@/services';
import {
loggedUserInfoStore,
siteInfoStore,
interfaceStore,
brandingStore,
loginSettingStore,
customizeStore,
themeSettingStore,
seoSettingStore,
loginToContinueStore,
pageTagStore,
writeSettingStore,
siteLealStore,
} from '@/stores';
import { RouteAlias } from '@/router/alias';
import {
LOGGED_TOKEN_STORAGE_KEY,
REDIRECT_PATH_STORAGE_KEY,
} from '@/common/constants';
import Storage from '@/utils/storage';
import { setupAppLanguage, setupAppTimeZone, setupAppTheme } from './localize';
import { floppyNavigation, NavigateConfig } from './floppyNavigation';
import { pullUcAgent, getSignUpUrl } from './userCenter';
type TLoginState = {
isLogged: boolean;
isNotActivated: boolean;
isActivated: boolean;
isNormal: boolean;
isAdmin: boolean;
isModerator: boolean;
};
export type TGuardResult = {
ok: boolean;
redirect?: string;
error?: {
code?: number | string;
msg?: string;
};
};
export type TGuardFunc = (args: {
loaderData?: any;
path?: string;
page?: string;
}) => TGuardResult;
export const deriveLoginState = (): TLoginState => {
const ls: TLoginState = {
isLogged: false,
isNotActivated: false,
isActivated: false,
isNormal: false,
isAdmin: false,
isModerator: false,
};
const { user } = loggedUserInfoStore.getState();
if (user.access_token) {
ls.isLogged = true;
}
if (ls.isLogged && user.mail_status === 1) {
ls.isActivated = true;
}
if (ls.isLogged && user.mail_status === 2) {
ls.isNotActivated = true;
}
if (ls.isActivated) {
ls.isNormal = true;
}
if (ls.isNormal && user.role_id === 2) {
ls.isAdmin = true;
}
if (ls.isNormal && user.role_id === 3) {
ls.isModerator = true;
}
return ls;
};
export const IGNORE_PATH_LIST = [
RouteAlias.login,
RouteAlias.signUp,
RouteAlias.accountRecovery,
RouteAlias.changeEmail,
RouteAlias.passwordReset,
RouteAlias.accountActivation,
RouteAlias.confirmNewEmail,
RouteAlias.confirmEmail,
RouteAlias.authLanding,
'/user-center/',
];
export const isIgnoredPath = (ignoredPath?: string | string[]) => {
if (!ignoredPath) {
ignoredPath = IGNORE_PATH_LIST;
}
if (!Array.isArray(ignoredPath)) {
ignoredPath = [ignoredPath];
}
const matchingPath = ignoredPath.find((p) => {
return floppyNavigation.matchToCurrentHref(p);
});
return !!matchingPath;
};
let pluTimestamp = 0;
export const pullLoggedUser = async (isInitPull = false) => {
/**
* WARN:
* - dedupe pull requests in this time span in 10 seconds
* - isInitPull:
* Requests sent by the initialisation method cannot be throttled
* and may cause Promise.allSettled to complete early in React development mode,
* resulting in inaccurate application data.
*/
//
if (!isInitPull && Date.now() - pluTimestamp < 1000 * 10) {
return;
}
pluTimestamp = Date.now();
const loggedUserInfo = await getLoggedUserInfo({
passingError: true,
}).catch(() => {
pluTimestamp = 0;
loggedUserInfoStore.getState().clear(false);
});
if (loggedUserInfo) {
loggedUserInfoStore.getState().update(loggedUserInfo);
}
};
export const logged = () => {
const gr: TGuardResult = { ok: true };
const us = deriveLoginState();
if (!us.isLogged) {
gr.ok = false;
gr.redirect = RouteAlias.login;
}
return gr;
};
export const loggedRedirectHome = () => {
const gr: TGuardResult = { ok: true };
const us = deriveLoginState();
if (!us.isLogged) {
gr.ok = false;
gr.redirect = RouteAlias.home;
}
return gr;
};
export const notLogged = () => {
const gr: TGuardResult = { ok: true };
const us = deriveLoginState();
if (us.isLogged) {
gr.ok = false;
gr.redirect = RouteAlias.home;
}
return gr;
};
export const notActivated = () => {
const gr: TGuardResult = { ok: true };
const us = deriveLoginState();
if (us.isActivated) {
gr.ok = false;
gr.redirect = RouteAlias.home;
}
return gr;
};
export const activated = () => {
const gr = logged();
const us = deriveLoginState();
if (us.isNotActivated) {
gr.ok = false;
gr.redirect = RouteAlias.inactive;
}
return gr;
};
export const admin = () => {
const gr = logged();
const us = deriveLoginState();
if (gr.ok && !us.isAdmin) {
gr.ok = false;
gr.error = {
code: '403',
msg: '',
};
gr.redirect = '';
}
return gr;
};
export const isAdminOrModerator = () => {
const gr = logged();
const us = deriveLoginState();
if (gr.ok && !us.isAdmin && !us.isModerator) {
gr.ok = false;
gr.error = {
code: '403',
msg: '',
};
gr.redirect = '';
}
return gr;
};
export const isEditable = (args) => {
const loaderData = args?.loaderData || {};
const gr: TGuardResult = { ok: true };
if (loaderData.code === 400) {
gr.ok = false;
gr.error = {
code: '403',
msg: loaderData.msg,
};
}
return gr;
};
export const allowNewRegistration = () => {
const gr: TGuardResult = { ok: true };
const loginSetting = loginSettingStore.getState().login;
if (!loginSetting.allow_new_registrations) {
gr.ok = false;
gr.redirect = RouteAlias.home;
}
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;
if (!loginSetting.login_required) {
return gr;
}
const us = deriveLoginState();
if (us.isLogged) {
return gr;
}
if (isIgnoredPath(IGNORE_PATH_LIST)) {
return gr;
}
gr.ok = false;
gr.redirect = RouteAlias.login;
return gr;
};
/**
* try user was logged and all state ok
* @param canNavigate // if true, will navigate to login page if not logged
*/
export const tryNormalLogged = (canNavigate: boolean = false) => {
const us = deriveLoginState();
if (us.isNormal) {
return true;
}
// must assert logged state first and return
if (!us.isLogged) {
if (canNavigate) {
loginToContinueStore.getState().update({ show: true });
}
return false;
}
if (us.isNotActivated) {
floppyNavigation.navigate(RouteAlias.inactive);
}
return false;
};
export const tryLoggedAndActivated = () => {
const gr: TGuardResult = { ok: true };
const us = deriveLoginState();
if (!us.isLogged || !us.isActivated) {
gr.ok = false;
}
return gr;
};
/**
* Auto handling of page redirect logic after a successful login
*/
export const handleLoginRedirect = (handler?: NavigateConfig['handler']) => {
const redirectUrl = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
floppyNavigation.navigate(redirectUrl, {
handler,
options: { replace: true },
});
};
/**
* Unified processing of login logic after getting `access_token`
*/
export const handleLoginWithToken = (
token: string | null,
handler?: NavigateConfig['handler'],
) => {
if (token) {
Storage.set(LOGGED_TOKEN_STORAGE_KEY, token);
setTimeout(() => {
getLoggedUserInfo().then((res) => {
loggedUserInfoStore.getState().update(res);
const userStat = deriveLoginState();
if (userStat.isNotActivated) {
floppyNavigation.navigate(RouteAlias.inactive, {
handler,
options: {
replace: true,
},
});
} else {
handleLoginRedirect(handler);
}
});
});
} else {
floppyNavigation.navigate(RouteAlias.home, {
handler,
options: {
replace: true,
},
});
}
};
/**
* Initialize app configuration
*/
export const initAppSettingsStore = async () => {
const appSettings = await getAppSettings();
if (appSettings) {
siteInfoStore.getState().update(appSettings.general);
siteInfoStore
.getState()
.updateVersion(appSettings.version, appSettings.revision);
siteInfoStore.getState().updateUsers(appSettings.site_users);
interfaceStore.getState().update(appSettings.interface);
pageTagStore.getState().update({
title: appSettings.general?.name,
description: appSettings.general?.description,
});
brandingStore.getState().update(appSettings.branding);
loginSettingStore.getState().update(appSettings.login);
customizeStore.getState().update(appSettings.custom_css_html);
themeSettingStore.getState().update(appSettings.theme);
seoSettingStore.getState().update(appSettings.site_seo);
writeSettingStore.getState().update({
restrict_answer: appSettings.site_write.restrict_answer,
...appSettings.site_write,
});
siteLealStore.getState().update({
external_content_display: appSettings.site_legal.external_content_display,
});
}
};
export const googleSnapshotRedirect = () => {
const gr: TGuardResult = { ok: true };
const searchStr = new URLSearchParams(window.location.search)?.get('q') || '';
if (window.location.host !== 'webcache.googleusercontent.com') {
return gr;
}
if (searchStr.indexOf('cache:') === 0 && searchStr.includes(':http')) {
const redirectUrl = `http${searchStr.split(':http')[1]}`;
const pathname = redirectUrl.replace(new URL(redirectUrl).origin, '');
gr.ok = false;
gr.redirect = pathname || '/';
return gr;
}
return gr;
};
let appInitialized = false;
export const setupApp = async () => {
/**
* This cannot be removed:
* clicking on the current navigation link will trigger a call to the routing loader,
* even though the page is not refreshed.
*/
if (appInitialized) {
return;
}
/**
* WARN:
* 1. must pre init logged user info for router guard
* 2. must pre init app settings for app render
*/
await Promise.allSettled([initAppSettingsStore(), pullLoggedUser(true)]);
await Promise.allSettled([pullUcAgent()]);
setupAppLanguage();
setupAppTimeZone();
setupAppTheme();
/**
* WARN:
* Initialization must be completed after all initialization actions,
* otherwise the problem of rendering twice in React development mode can lead to inaccurate data or flickering pages
*/
appInitialized = true;
};