fix(cloudly): invalidate expired dashboard sessions
This commit is contained in:
@@ -9,6 +9,10 @@
|
|||||||
- Stores Spark metrics and runtime info on cluster node records
|
- Stores Spark metrics and runtime info on cluster node records
|
||||||
- Extends jump onboarding with per-node Spark telemetry credentials
|
- Extends jump onboarding with per-node Spark telemetry credentials
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- invalidate expired dashboard sessions and return admins to login
|
||||||
|
|
||||||
### Maintenance
|
### Maintenance
|
||||||
|
|
||||||
- refresh release tooling dependencies
|
- refresh release tooling dependencies
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ export interface IJwtData {
|
|||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IReq_AdminValidateIdentity {
|
||||||
|
method: 'adminValidateIdentity';
|
||||||
|
request: {
|
||||||
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
valid: boolean;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class CloudlyAuthManager {
|
export class CloudlyAuthManager {
|
||||||
cloudlyRef: Cloudly;
|
cloudlyRef: Cloudly;
|
||||||
public get db() {
|
public get db() {
|
||||||
@@ -82,6 +93,16 @@ export class CloudlyAuthManager {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<IReq_AdminValidateIdentity>('adminValidateIdentity', async (dataArg) => {
|
||||||
|
const valid = await this.adminIdentityGuard.exec(dataArg).catch(() => false);
|
||||||
|
return {
|
||||||
|
valid,
|
||||||
|
reason: valid ? undefined : 'identity is not valid',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async bootstrapInitialAdmin() {
|
private async bootstrapInitialAdmin() {
|
||||||
@@ -126,28 +147,22 @@ export class CloudlyAuthManager {
|
|||||||
identity: plugins.servezoneInterfaces.data.IIdentity;
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
||||||
}>(
|
}>(
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
const jwt = dataArg.identity.jwt;
|
try {
|
||||||
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
const jwt = dataArg.identity?.jwt;
|
||||||
const expired = jwtData.expiresAt < Date.now();
|
if (!jwt) {
|
||||||
plugins.smartexpect
|
return false;
|
||||||
.expect(jwtData.status)
|
}
|
||||||
.setFailMessage('user not logged in')
|
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
||||||
.toEqual('loggedIn');
|
const expired = jwtData.expiresAt < Date.now();
|
||||||
plugins.smartexpect.expect(expired).setFailMessage(`jwt expired`).toBeFalse();
|
return (
|
||||||
plugins.smartexpect
|
jwtData.status === 'loggedIn' &&
|
||||||
.expect(dataArg.identity.expiresAt)
|
!expired &&
|
||||||
.setFailMessage(
|
dataArg.identity.expiresAt === jwtData.expiresAt &&
|
||||||
`expiresAt >>identity valid until:${dataArg.identity.expiresAt}, but jwt says: ${jwtData.expiresAt}<< has been tampered with`,
|
dataArg.identity.userId === jwtData.userId
|
||||||
)
|
);
|
||||||
.toEqual(jwtData.expiresAt);
|
} catch {
|
||||||
plugins.smartexpect
|
return false;
|
||||||
.expect(dataArg.identity.userId)
|
|
||||||
.setFailMessage('userId has been tampered with')
|
|
||||||
.toEqual(jwtData.userId);
|
|
||||||
if (expired) {
|
|
||||||
throw new Error('identity is expired');
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
failedHint: 'identity is not valid.',
|
failedHint: 'identity is not valid.',
|
||||||
@@ -159,16 +174,17 @@ export class CloudlyAuthManager {
|
|||||||
identity: plugins.servezoneInterfaces.data.IIdentity;
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
||||||
}>(
|
}>(
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await plugins.smartguard.passGuardsOrReject(dataArg, [this.validIdentityGuard]);
|
const validIdentity = await this.validIdentityGuard.exec(dataArg);
|
||||||
|
if (!validIdentity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const jwt = dataArg.identity.jwt;
|
const jwt = dataArg.identity.jwt;
|
||||||
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
||||||
const user = await this.CUser.getInstance({ id: jwtData.userId });
|
const user = await this.CUser.getInstance({ id: jwtData.userId });
|
||||||
const isAdminBool = user.data.role === 'admin';
|
return user?.data.role === 'admin';
|
||||||
console.log(`user is admin: ${isAdminBool}`);
|
|
||||||
return isAdminBool;
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
failedHint: 'user is not admin.',
|
failedHint: 'identity is not valid or user is not admin.',
|
||||||
name: 'adminIdentityGuard',
|
name: 'adminIdentityGuard',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -177,14 +193,17 @@ export class CloudlyAuthManager {
|
|||||||
identity: plugins.servezoneInterfaces.data.IIdentity;
|
identity: plugins.servezoneInterfaces.data.IIdentity;
|
||||||
}>(
|
}>(
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await plugins.smartguard.passGuardsOrReject(dataArg, [this.validIdentityGuard]);
|
const validIdentity = await this.validIdentityGuard.exec(dataArg);
|
||||||
|
if (!validIdentity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const jwt = dataArg.identity.jwt;
|
const jwt = dataArg.identity.jwt;
|
||||||
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
|
||||||
const user = await this.CUser.getInstance({ id: jwtData.userId });
|
const user = await this.CUser.getInstance({ id: jwtData.userId });
|
||||||
return user.data.role === 'admin' || user.data.role === 'cluster';
|
return user?.data.role === 'admin' || user?.data.role === 'cluster';
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
failedHint: 'user is not admin or cluster.',
|
failedHint: 'identity is not valid or user is not admin or cluster.',
|
||||||
name: 'adminOrClusterIdentityGuard',
|
name: 'adminOrClusterIdentityGuard',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
+164
-37
@@ -16,6 +16,55 @@ export interface IUiState {
|
|||||||
activeSubview: string | null;
|
activeSubview: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDataState {
|
||||||
|
secretGroups?: plugins.interfaces.data.ISecretGroup[];
|
||||||
|
secretBundles?: plugins.interfaces.data.ISecretBundle[];
|
||||||
|
clusters?: plugins.interfaces.data.ICluster[];
|
||||||
|
externalRegistries?: plugins.interfaces.data.IExternalRegistry[];
|
||||||
|
images?: any[];
|
||||||
|
services?: plugins.interfaces.data.IService[];
|
||||||
|
deployments?: plugins.interfaces.data.IDeployment[];
|
||||||
|
domains?: plugins.interfaces.data.IDomain[];
|
||||||
|
dnsEntries?: plugins.interfaces.data.IDnsEntry[];
|
||||||
|
tasks?: any[];
|
||||||
|
taskExecutions?: plugins.interfaces.data.ITaskExecution[];
|
||||||
|
mails?: any[];
|
||||||
|
logs?: any[];
|
||||||
|
s3?: any[];
|
||||||
|
dbs?: any[];
|
||||||
|
backups?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyDataState: IDataState = {
|
||||||
|
secretGroups: [],
|
||||||
|
secretBundles: [],
|
||||||
|
clusters: [],
|
||||||
|
externalRegistries: [],
|
||||||
|
images: [],
|
||||||
|
services: [],
|
||||||
|
deployments: [],
|
||||||
|
domains: [],
|
||||||
|
dnsEntries: [],
|
||||||
|
tasks: [],
|
||||||
|
taskExecutions: [],
|
||||||
|
mails: [],
|
||||||
|
logs: [],
|
||||||
|
s3: [],
|
||||||
|
dbs: [],
|
||||||
|
backups: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IReq_AdminValidateIdentity {
|
||||||
|
method: 'adminValidateIdentity';
|
||||||
|
request: {
|
||||||
|
identity: plugins.interfaces.data.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
valid: boolean;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const getInitialView = (): string => {
|
const getInitialView = (): string => {
|
||||||
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||||
const validViews = ['overview', 'platform', 'runtime', 'registry', 'secrets', 'domains', 'storage', 'logs'];
|
const validViews = ['overview', 'platform', 'runtime', 'registry', 'secrets', 'domains', 'storage', 'logs'];
|
||||||
@@ -49,7 +98,7 @@ export const loginAction = loginStatePart.createAction<{ username: string; passw
|
|||||||
}
|
}
|
||||||
const newState = {
|
const newState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
...(identity ? { identity } : {}),
|
identity,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
// Keep shared API client in sync and establish WS for modules using sockets
|
// Keep shared API client in sync and establish WS for modules using sockets
|
||||||
@@ -67,50 +116,19 @@ export const loginAction = loginStatePart.createAction<{ username: string; passw
|
|||||||
|
|
||||||
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
||||||
const currentState = statePartArg.getState() || { identity: null };
|
const currentState = statePartArg.getState() || { identity: null };
|
||||||
|
try {
|
||||||
|
apiClient.identity = null;
|
||||||
|
dataState.setState({ ...emptyDataState });
|
||||||
|
} catch {}
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
identity: null,
|
identity: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface IDataState {
|
|
||||||
secretGroups?: plugins.interfaces.data.ISecretGroup[];
|
|
||||||
secretBundles?: plugins.interfaces.data.ISecretBundle[];
|
|
||||||
clusters?: plugins.interfaces.data.ICluster[];
|
|
||||||
externalRegistries?: plugins.interfaces.data.IExternalRegistry[];
|
|
||||||
images?: any[];
|
|
||||||
services?: plugins.interfaces.data.IService[];
|
|
||||||
deployments?: plugins.interfaces.data.IDeployment[];
|
|
||||||
domains?: plugins.interfaces.data.IDomain[];
|
|
||||||
dnsEntries?: plugins.interfaces.data.IDnsEntry[];
|
|
||||||
tasks?: any[];
|
|
||||||
taskExecutions?: plugins.interfaces.data.ITaskExecution[];
|
|
||||||
mails?: any[];
|
|
||||||
logs?: any[];
|
|
||||||
s3?: any[];
|
|
||||||
dbs?: any[];
|
|
||||||
backups?: any[];
|
|
||||||
}
|
|
||||||
export const dataState = await appstate.getStatePart<IDataState>(
|
export const dataState = await appstate.getStatePart<IDataState>(
|
||||||
'data',
|
'data',
|
||||||
{
|
{ ...emptyDataState },
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
clusters: [],
|
|
||||||
externalRegistries: [],
|
|
||||||
images: [],
|
|
||||||
services: [],
|
|
||||||
deployments: [],
|
|
||||||
domains: [],
|
|
||||||
dnsEntries: [],
|
|
||||||
tasks: [],
|
|
||||||
taskExecutions: [],
|
|
||||||
mails: [],
|
|
||||||
logs: [],
|
|
||||||
s3: [],
|
|
||||||
dbs: [],
|
|
||||||
backups: [],
|
|
||||||
},
|
|
||||||
'soft'
|
'soft'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -124,6 +142,115 @@ export const apiClient = new plugins.servezoneApi.CloudlyApiClient({
|
|||||||
cloudlyUrl: (typeof window !== 'undefined' && window.location?.origin) ? window.location.origin : undefined,
|
cloudlyUrl: (typeof window !== 'undefined' && window.location?.origin) ? window.location.origin : undefined,
|
||||||
}) as TCloudlyApiClientWithNullableIdentity;
|
}) as TCloudlyApiClientWithNullableIdentity;
|
||||||
|
|
||||||
|
let identityExpiryTimer: number | undefined;
|
||||||
|
let identityInvalidationRunning = false;
|
||||||
|
|
||||||
|
const getErrorText = (errorArg: unknown): string => {
|
||||||
|
if (!errorArg) return '';
|
||||||
|
if (typeof errorArg === 'string') return errorArg;
|
||||||
|
const errorLike = errorArg as { errorText?: string; message?: string; text?: string };
|
||||||
|
return errorLike.errorText || errorLike.message || errorLike.text || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAuthRejectionText = (errorTextArg: string): boolean => {
|
||||||
|
const errorText = errorTextArg.toLowerCase();
|
||||||
|
return [
|
||||||
|
'identity is not valid',
|
||||||
|
'jwt expired',
|
||||||
|
'identity is expired',
|
||||||
|
'user not logged in',
|
||||||
|
'has been tampered with',
|
||||||
|
'invalid jwt',
|
||||||
|
'invalid signature',
|
||||||
|
].some((textPart) => errorText.includes(textPart));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isIdentityExpired = (identityArg: plugins.interfaces.data.IIdentity | null | undefined): boolean => {
|
||||||
|
return typeof identityArg?.expiresAt === 'number' && identityArg.expiresAt <= Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const invalidateIdentity = async (reasonArg = 'identity is not valid'): Promise<void> => {
|
||||||
|
if (identityInvalidationRunning) return;
|
||||||
|
identityInvalidationRunning = true;
|
||||||
|
try {
|
||||||
|
const currentLoginState = loginStatePart.getState() || { identity: null };
|
||||||
|
if (currentLoginState.identity) {
|
||||||
|
console.warn(`Cloudly session invalidated: ${reasonArg}`);
|
||||||
|
}
|
||||||
|
apiClient.identity = null;
|
||||||
|
try { await apiClient.typedsocketClient?.setTag('identity', null); } catch {}
|
||||||
|
loginStatePart.setState({
|
||||||
|
...currentLoginState,
|
||||||
|
identity: null,
|
||||||
|
});
|
||||||
|
dataState.setState({ ...emptyDataState });
|
||||||
|
} finally {
|
||||||
|
identityInvalidationRunning = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateStoredIdentity = async (): Promise<boolean> => {
|
||||||
|
const identity = loginStatePart.getState()?.identity ?? null;
|
||||||
|
if (!identity) return false;
|
||||||
|
|
||||||
|
if (isIdentityExpired(identity)) {
|
||||||
|
await invalidateIdentity('identity expired');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateIdentityRequest = new plugins.typedrequest.TypedRequest<IReq_AdminValidateIdentity>(
|
||||||
|
'/typedrequest',
|
||||||
|
'adminValidateIdentity',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await validateIdentityRequest.fire({ identity });
|
||||||
|
if (!response?.valid) {
|
||||||
|
await invalidateIdentity(response?.reason || 'identity rejected by server');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorText = getErrorText(error);
|
||||||
|
if (isAuthRejectionText(errorText)) {
|
||||||
|
await invalidateIdentity(errorText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.warn('Could not validate stored identity:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!loginStatePart.getState()?.identity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleIdentityExpiryTimer = () => {
|
||||||
|
if (identityExpiryTimer) {
|
||||||
|
window.clearTimeout(identityExpiryTimer);
|
||||||
|
identityExpiryTimer = undefined;
|
||||||
|
}
|
||||||
|
const identity = loginStatePart.getState()?.identity ?? null;
|
||||||
|
if (!identity?.expiresAt) return;
|
||||||
|
const msUntilExpiry = identity.expiresAt - Date.now();
|
||||||
|
if (msUntilExpiry <= 0) {
|
||||||
|
void invalidateIdentity('identity expired');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
identityExpiryTimer = window.setTimeout(() => {
|
||||||
|
void invalidateIdentity('identity expired');
|
||||||
|
}, Math.min(msUntilExpiry, 2147483647));
|
||||||
|
};
|
||||||
|
|
||||||
|
plugins.typedrequest.TypedRouter.setGlobalHooks({
|
||||||
|
onIncomingResponse: (entryArg) => {
|
||||||
|
if (entryArg.error && isAuthRejectionText(entryArg.error)) {
|
||||||
|
void invalidateIdentity(entryArg.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
loginStatePart.select((stateArg) => stateArg?.identity ?? null).subscribe(() => {
|
||||||
|
scheduleIdentityExpiryTimer();
|
||||||
|
});
|
||||||
|
scheduleIdentityExpiryTimer();
|
||||||
|
|
||||||
// Getting data
|
// Getting data
|
||||||
export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
||||||
let currentState = statePartArg.getState() || {};
|
let currentState = statePartArg.getState() || {};
|
||||||
|
|||||||
@@ -169,6 +169,17 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
this.syncAppdashView(uiState.activeView, uiState.activeSubview);
|
this.syncAppdashView(uiState.activeView, uiState.activeSubview);
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(uiSubscription);
|
this.rxSubscriptions.push(uiSubscription);
|
||||||
|
|
||||||
|
const loginSubscription = appstate.loginStatePart
|
||||||
|
.select((stateArg) => stateArg?.identity ?? null)
|
||||||
|
.subscribe((identityArg) => {
|
||||||
|
const hadIdentity = !!this.identity;
|
||||||
|
this.identity = identityArg ?? null;
|
||||||
|
if (!identityArg && hadIdentity) {
|
||||||
|
void this.switchToLoginContent('Session expired. Please sign in again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(loginSubscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncAppdashView(viewSlug: string, subviewSlug: string | null): void {
|
private syncAppdashView(viewSlug: string, subviewSlug: string | null): void {
|
||||||
@@ -267,9 +278,16 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
const domtools = await this.domtoolsPromise;
|
const domtools = await this.domtoolsPromise;
|
||||||
const loginState = appstate.loginStatePart.getState();
|
const loginState = appstate.loginStatePart.getState();
|
||||||
if (loginState?.identity) {
|
if (loginState?.identity) {
|
||||||
this.identity = loginState.identity;
|
const identityValid = await appstate.validateStoredIdentity();
|
||||||
|
const currentIdentity = appstate.loginStatePart.getState()?.identity ?? null;
|
||||||
|
if (!identityValid || !currentIdentity) {
|
||||||
|
await this.switchToLoginContent('Session expired. Please sign in again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.identity = currentIdentity;
|
||||||
try {
|
try {
|
||||||
appstate.apiClient.identity = loginState.identity;
|
appstate.apiClient.identity = currentIdentity;
|
||||||
if (!appstate.apiClient['typedsocketClient']) {
|
if (!appstate.apiClient['typedsocketClient']) {
|
||||||
await appstate.apiClient.start();
|
await appstate.apiClient.start();
|
||||||
}
|
}
|
||||||
@@ -301,5 +319,34 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async logout() {}
|
private async switchToLoginContent(statusMessageArg?: string) {
|
||||||
|
const simpleLogin = this.shadowRoot?.querySelector('dees-simple-login') as any;
|
||||||
|
if (!simpleLogin?.shadowRoot) return;
|
||||||
|
|
||||||
|
const loginDiv = simpleLogin.shadowRoot.querySelector('.login') as HTMLDivElement | null;
|
||||||
|
const loginContainerDiv = simpleLogin.shadowRoot.querySelector('.loginContainer') as HTMLDivElement | null;
|
||||||
|
const slotContainerDiv = simpleLogin.shadowRoot.querySelector('.slotContainer') as HTMLDivElement | null;
|
||||||
|
const form = simpleLogin.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
|
||||||
|
if (loginDiv) {
|
||||||
|
loginDiv.style.opacity = '1';
|
||||||
|
loginDiv.style.transform = 'translateY(0px)';
|
||||||
|
}
|
||||||
|
if (loginContainerDiv) {
|
||||||
|
loginContainerDiv.style.pointerEvents = 'all';
|
||||||
|
}
|
||||||
|
if (slotContainerDiv) {
|
||||||
|
slotContainerDiv.style.opacity = '0';
|
||||||
|
slotContainerDiv.style.transform = 'translateY(20px)';
|
||||||
|
slotContainerDiv.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
if (form && statusMessageArg) {
|
||||||
|
form.setStatus('error', statusMessageArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async logout() {
|
||||||
|
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||||
|
await this.switchToLoginContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -4,6 +4,11 @@ export {
|
|||||||
interfaces
|
interfaces
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @api.global scope
|
||||||
|
import * as typedrequest from '@api.global/typedrequest';
|
||||||
|
|
||||||
|
export { typedrequest };
|
||||||
|
|
||||||
// @design.estate scope
|
// @design.estate scope
|
||||||
import * as deesDomtools from '@design.estate/dees-domtools';
|
import * as deesDomtools from '@design.estate/dees-domtools';
|
||||||
import * as deesElement from '@design.estate/dees-element';
|
import * as deesElement from '@design.estate/dees-element';
|
||||||
@@ -11,11 +16,6 @@ import * as deesCatalog from '@design.estate/dees-catalog';
|
|||||||
|
|
||||||
export { deesDomtools, deesElement, deesCatalog };
|
export { deesDomtools, deesElement, deesCatalog };
|
||||||
|
|
||||||
// @api.global scope
|
|
||||||
import * as typedrequest from '@api.global/typedrequest';
|
|
||||||
|
|
||||||
export { typedrequest };
|
|
||||||
|
|
||||||
// @push.rocks scope
|
// @push.rocks scope
|
||||||
import * as webjwt from '@push.rocks/webjwt';
|
import * as webjwt from '@push.rocks/webjwt';
|
||||||
import * as smartstate from '@push.rocks/smartstate';
|
import * as smartstate from '@push.rocks/smartstate';
|
||||||
|
|||||||
Reference in New Issue
Block a user