fix: blank screen in admin dashboard on authentication problems (#3385)
* fix: dashboard logs out user when cookie expires or is unauthorized * fix: handles the 401 error thrown when trying to refresh tokens * chore: updated wrong logic when returning state in refresh token function * feat: introduced auth exchange to urql client to check for errors on each backend call * fix: prevent multiple window reloads --------- Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
committed by
GitHub
parent
0188a8d7db
commit
b18fd90b64
@@ -22,7 +22,8 @@
|
|||||||
"@intlify/unplugin-vue-i18n": "^1.2.0",
|
"@intlify/unplugin-vue-i18n": "^1.2.0",
|
||||||
"@types/cors": "^2.8.13",
|
"@types/cors": "^2.8.13",
|
||||||
"@types/express": "^4.17.15",
|
"@types/express": "^4.17.15",
|
||||||
"@urql/vue": "^1.0.4",
|
"@urql/exchange-auth": "^2.1.6",
|
||||||
|
"@urql/vue": "^1.1.2",
|
||||||
"@vueuse/core": "^9.10.0",
|
"@vueuse/core": "^9.10.0",
|
||||||
"axios": "^0.21.4",
|
"axios": "^0.21.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
63
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
63
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
@@ -1,42 +1,39 @@
|
|||||||
// generated by unplugin-vue-components
|
// generated by unplugin-vue-components
|
||||||
// We suggest you to commit this file into source control
|
// We suggest you to commit this file into source control
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
import '@vue/runtime-core'
|
import '@vue/runtime-core';
|
||||||
|
|
||||||
export {}
|
export {};
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
AppHeader: typeof import('./components/app/Header.vue')['default'];
|
||||||
AppLogin: typeof import('./components/app/Login.vue')['default']
|
AppLogin: typeof import('./components/app/Login.vue')['default'];
|
||||||
AppLogout: typeof import('./components/app/Logout.vue')['default']
|
AppLogout: typeof import('./components/app/Logout.vue')['default'];
|
||||||
AppModal: typeof import('./components/app/Modal.vue')['default']
|
AppModal: typeof import('./components/app/Modal.vue')['default'];
|
||||||
AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
|
AppSidebar: typeof import('./components/app/Sidebar.vue')['default'];
|
||||||
AppToast: typeof import('./components/app/Toast.vue')['default']
|
AppToast: typeof import('./components/app/Toast.vue')['default'];
|
||||||
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']
|
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'];
|
||||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'];
|
||||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'];
|
||||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'];
|
||||||
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete'];
|
||||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'];
|
||||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'];
|
||||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'];
|
||||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'];
|
||||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'];
|
||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'];
|
||||||
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable']
|
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'];
|
||||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
IconLucideInbox: typeof import('~icons/lucide/inbox')['default'];
|
||||||
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
|
TeamsAdd: typeof import('./components/teams/Add.vue')['default'];
|
||||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
TeamsDetails: typeof import('./components/teams/Details.vue')['default'];
|
||||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
|
TeamsInvite: typeof import('./components/teams/Invite.vue')['default'];
|
||||||
TeamsDetails: typeof import('./components/teams/Details.vue')['default']
|
TeamsMembers: typeof import('./components/teams/Members.vue')['default'];
|
||||||
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
|
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default'];
|
||||||
TeamsMembers: typeof import('./components/teams/Members.vue')['default']
|
TeamsTable: typeof import('./components/teams/Table.vue')['default'];
|
||||||
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default']
|
Tippy: typeof import('vue-tippy')['Tippy'];
|
||||||
TeamsTable: typeof import('./components/teams/Table.vue')['default']
|
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'];
|
||||||
Tippy: typeof import('vue-tippy')['Tippy']
|
UsersTable: typeof import('./components/users/Table.vue')['default'];
|
||||||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
|
|
||||||
UsersTable: typeof import('./components/users/Table.vue')['default']
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
setLocalConfig,
|
setLocalConfig,
|
||||||
} from './localpersistence';
|
} from './localpersistence';
|
||||||
import { Ref, ref, watch } from 'vue';
|
import { Ref, ref, watch } from 'vue';
|
||||||
|
import * as O from 'fp-ts/Option';
|
||||||
/**
|
/**
|
||||||
* A common (and required) set of fields that describe a user.
|
* A common (and required) set of fields that describe a user.
|
||||||
*/
|
*/
|
||||||
@@ -60,6 +60,24 @@ async function logout() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signOut = async (reloadWindow = false) => {
|
||||||
|
await logout();
|
||||||
|
|
||||||
|
// Reload the window if both `access_token` and `refresh_token`is invalid
|
||||||
|
// there by the user is taken to the login page
|
||||||
|
if (reloadWindow) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
probableUser$.next(null);
|
||||||
|
currentUser$.next(null);
|
||||||
|
removeLocalConfig('login_state');
|
||||||
|
|
||||||
|
authEvents$.next({
|
||||||
|
event: 'logout',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
async function signInUserWithGithubFB() {
|
async function signInUserWithGithubFB() {
|
||||||
window.location.href = `${
|
window.location.href = `${
|
||||||
import.meta.env.VITE_BACKEND_API_URL
|
import.meta.env.VITE_BACKEND_API_URL
|
||||||
@@ -149,6 +167,7 @@ async function setInitialUser() {
|
|||||||
setInitialUser();
|
setInitialUser();
|
||||||
} else {
|
} else {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
|
await signOut(true);
|
||||||
isGettingInitialUser.value = false;
|
isGettingInitialUser.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,24 +206,22 @@ async function setInitialUser() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshToken() {
|
const refreshToken = async () => {
|
||||||
const res = await axios.get(
|
try {
|
||||||
`${import.meta.env.VITE_BACKEND_API_URL}/auth/refresh`,
|
const res = await axios.get(
|
||||||
{
|
`${import.meta.env.VITE_BACKEND_API_URL}/auth/refresh`,
|
||||||
withCredentials: true,
|
{
|
||||||
}
|
withCredentials: true,
|
||||||
);
|
}
|
||||||
|
);
|
||||||
const isSuccessful = res.status === 200;
|
|
||||||
|
|
||||||
if (isSuccessful) {
|
|
||||||
authEvents$.next({
|
authEvents$.next({
|
||||||
event: 'token_refresh',
|
event: 'token_refresh',
|
||||||
});
|
});
|
||||||
|
return res.status === 200;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
return isSuccessful;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function elevateUser() {
|
async function elevateUser() {
|
||||||
const res = await axios.get(
|
const res = await axios.get(
|
||||||
@@ -356,18 +373,21 @@ export const auth = {
|
|||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|
||||||
async signOutUser() {
|
async performAuthRefresh() {
|
||||||
// if (!currentUser$.value) throw new Error("No user has logged in")
|
const isRefreshSuccess = await refreshToken();
|
||||||
|
|
||||||
await logout();
|
if (isRefreshSuccess) {
|
||||||
|
setInitialUser();
|
||||||
|
return O.some(true);
|
||||||
|
} else {
|
||||||
|
setUser(null);
|
||||||
|
isGettingInitialUser.value = false;
|
||||||
|
return O.none;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
probableUser$.next(null);
|
async signOutUser(reloadWindow = false) {
|
||||||
currentUser$.next(null);
|
await signOut(reloadWindow);
|
||||||
removeLocalConfig('login_state');
|
|
||||||
|
|
||||||
authEvents$.next({
|
|
||||||
event: 'logout',
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async processMagicLink() {
|
async processMagicLink() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import urql, { createClient } from '@urql/vue';
|
import urql, { createClient, cacheExchange, fetchExchange } from '@urql/vue';
|
||||||
|
import { authExchange } from '@urql/exchange-auth';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
|
||||||
// STYLES
|
// STYLES
|
||||||
@@ -11,9 +12,10 @@ import '@fontsource-variable/inter';
|
|||||||
import '@fontsource-variable/material-symbols-rounded';
|
import '@fontsource-variable/material-symbols-rounded';
|
||||||
import '@fontsource-variable/roboto-mono';
|
import '@fontsource-variable/roboto-mono';
|
||||||
// END STYLES
|
// END STYLES
|
||||||
|
|
||||||
import { HOPP_MODULES } from './modules';
|
import { HOPP_MODULES } from './modules';
|
||||||
import { auth } from './helpers/auth';
|
import { auth } from './helpers/auth';
|
||||||
|
import { pipe } from 'fp-ts/function';
|
||||||
|
import * as O from 'fp-ts/Option';
|
||||||
|
|
||||||
// Top-level await is not available in our targets
|
// Top-level await is not available in our targets
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -27,6 +29,28 @@ import { auth } from './helpers/auth';
|
|||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
exchanges: [
|
||||||
|
cacheExchange,
|
||||||
|
authExchange(async () => {
|
||||||
|
return {
|
||||||
|
addAuthToOperation(operation) {
|
||||||
|
return operation;
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshAuth() {
|
||||||
|
pipe(
|
||||||
|
await auth.performAuthRefresh(),
|
||||||
|
O.getOrElseW(async () => await auth.signOutUser(true))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
didAuthError(error, _operation) {
|
||||||
|
return error.message === '[GraphQL] Unauthorized';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
fetchExchange,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
3308
pnpm-lock.yaml
generated
3308
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user