Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f8ef6b985 |
+61
-18
@@ -2,32 +2,74 @@ import {ApolloClient, ApolloLink, InMemoryCache} from '@apollo/client/core'
|
||||
import {ErrorLink} from "@apollo/client/link/error";
|
||||
import {defineStore} from "pinia";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {Notify} from "quasar";
|
||||
import {Dialog, Notify} from "quasar";
|
||||
import {errorObject, warningObject} from "src/composables/dialog";
|
||||
import {useAuthStore} from "stores/Auth";
|
||||
import UploadHttpLink from "apollo-upload-client/UploadHttpLink.mjs";
|
||||
import LoginOverlayDialog from "components/auth/LoginOverlayDialog.vue";
|
||||
|
||||
// Dedup across concurrently failing operations: if a query and a mutation
|
||||
// both 401 around the same time, they must share one login overlay instead
|
||||
// of each opening its own dialog. Module-level state is fine here (unlike
|
||||
// the SSR-sensitive apolloWrapper below) because this path only ever runs
|
||||
// client-side, one browser tab per module instance.
|
||||
let pendingLoginPromise = null;
|
||||
|
||||
const showLoginOverlay = (store) => {
|
||||
if (pendingLoginPromise) return pendingLoginPromise;
|
||||
|
||||
Notify.create(warningObject("Tvoje přihlášení vypršelo. Přihlaš se prosím znovu."));
|
||||
|
||||
pendingLoginPromise = new Promise((resolve) => {
|
||||
Dialog.create({
|
||||
component: LoginOverlayDialog,
|
||||
persistent: true,
|
||||
}).onOk(() => resolve(useAuthStore(store).getAccessToken()));
|
||||
}).finally(() => {
|
||||
pendingLoginPromise = null;
|
||||
});
|
||||
|
||||
return pendingLoginPromise;
|
||||
};
|
||||
|
||||
const retryWithToken = (operation, forward) => (accessToken) => {
|
||||
const oldHeaders = operation.getContext().headers;
|
||||
operation.setContext({
|
||||
headers: {
|
||||
...oldHeaders,
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
// retry the request, returning the new observable
|
||||
return forward(operation);
|
||||
};
|
||||
|
||||
const handleRefreshToken = (store, router, forward, operation) => {
|
||||
if (!operation.query?._refreshUnauthorizedUser) return;
|
||||
|
||||
console.log("refreshing token, ", operation.query);
|
||||
return useAuthStore(store).refreshAccessToken()
|
||||
.catch((error) => {
|
||||
Notify.create(warningObject("Tvoje přihlášení vypršelo. Přihlaš se prosím znovu."));
|
||||
router.push({name: "login"})
|
||||
})
|
||||
.then(value => value.flatMap((accessToken) => {
|
||||
const oldHeaders = operation.getContext().headers;
|
||||
operation.setContext({
|
||||
headers: {
|
||||
...oldHeaders,
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
// Loop guard: if the operation retried after a refresh/login still comes
|
||||
// back 401, don't try forever - surface a hard error instead.
|
||||
if (operation.getContext()._tokenRefreshAttempted) {
|
||||
Notify.create(errorObject("Přihlášení se nezdařilo, zkus to prosím znovu."));
|
||||
return Promise.reject(new Error("Token refresh retry failed"));
|
||||
}
|
||||
operation.setContext({_tokenRefreshAttempted: true});
|
||||
|
||||
// retry the request, returning the new observable
|
||||
return forward(operation);
|
||||
}))
|
||||
const retry = retryWithToken(operation, forward);
|
||||
|
||||
// The overlay only makes sense client-side (no DOM during SSR); keep the
|
||||
// previous redirect-to-login behavior there.
|
||||
const onRefreshFailed = process.env.SERVER
|
||||
? () => {
|
||||
router.push({name: "login"});
|
||||
return Promise.reject(new Error("Token refresh failed (SSR)"));
|
||||
}
|
||||
: () => showLoginOverlay(store).then(retry);
|
||||
|
||||
return useAuthStore(store).refreshAccessToken()
|
||||
.then(retry)
|
||||
.catch(onRefreshFailed);
|
||||
}
|
||||
|
||||
// SSR: boot() runs again for every request. The client/query/mutate below
|
||||
@@ -124,6 +166,7 @@ export const useApolloStore = defineStore('Apollo', () => {
|
||||
.then((response) => response)
|
||||
.catch(err => {
|
||||
console.error("Error during mutation: ", err);
|
||||
return Promise.reject(err);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<q-dialog ref="dialogRef" persistent @hide="onDialogHide">
|
||||
<q-card style="width: 95%; max-width: 400px;">
|
||||
<q-form @submit.prevent="logUser">
|
||||
<q-card-section class="bg-primary">
|
||||
<h4 class="text-h6 q-my-md text-white">Přihlášení vypršelo</h4>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<p>Tvoje přihlášení vypršelo. Přihlaš se prosím znovu, aby se neztratila rozpracovaná data.</p>
|
||||
<q-input v-model="formData.email" :rules="rules.email" type="email" label="E-mail">
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="email"/>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input v-model="formData.password" :rules="rules.password" type="password" label="Heslo">
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="lock"/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
<q-card-actions>
|
||||
<q-btn type="submit" unelevated size="lg" color="primary" class="full-width" :loading="loading" label="Přihlásit se"/>
|
||||
</q-card-actions>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {reactive, ref} from "vue";
|
||||
import {useDialogPluginComponent, useQuasar} from "quasar";
|
||||
import {isRequired, isValidEmail, minLength} from "src/composables/form/rules";
|
||||
import {errorObject} from "src/composables/dialog";
|
||||
import {useAuthStore} from "stores/Auth";
|
||||
|
||||
const {dialogRef, onDialogHide, onDialogOK} = useDialogPluginComponent();
|
||||
defineEmits([...useDialogPluginComponent.emits]);
|
||||
|
||||
const $q = useQuasar();
|
||||
const loading = ref(false);
|
||||
const formData = reactive({
|
||||
email: "",
|
||||
password: ""
|
||||
});
|
||||
const rules = {
|
||||
email: [isRequired, isValidEmail],
|
||||
password: [isRequired, minLength(4)]
|
||||
};
|
||||
|
||||
const logUser = () => {
|
||||
loading.value = true;
|
||||
useAuthStore().login(formData)
|
||||
.then(() => onDialogOK())
|
||||
.catch(() => $q.notify(errorObject("Přihlášení se nezdařilo.")))
|
||||
.finally(() => loading.value = false);
|
||||
};
|
||||
</script>
|
||||
+22
-8
@@ -42,14 +42,28 @@ export const useAuthStore = defineStore('Auth', () => {
|
||||
|
||||
const hasAccessToken = () => !!getAccessToken();
|
||||
|
||||
const refreshAccessToken = async () => api.post("/refresh")
|
||||
.then(({data}) => {
|
||||
setTokens(data);
|
||||
return data.access_token;
|
||||
}).catch((e) => {
|
||||
console.log("cannot refresh token", e);
|
||||
return Promise.reject(e);
|
||||
})
|
||||
// In-flight guard: if several requests 401 at the same time, they must all
|
||||
// await the same refresh call instead of each firing its own POST /refresh
|
||||
// (thundering herd, and a real risk if the backend rotates the refresh token
|
||||
// on use).
|
||||
let refreshPromise = null;
|
||||
|
||||
const refreshAccessToken = async () => {
|
||||
if (refreshPromise) return refreshPromise;
|
||||
|
||||
refreshPromise = api.post("/refresh")
|
||||
.then(({data}) => {
|
||||
setTokens(data);
|
||||
return data.access_token;
|
||||
}).catch((e) => {
|
||||
console.log("cannot refresh token", e);
|
||||
return Promise.reject(e);
|
||||
}).finally(() => {
|
||||
refreshPromise = null;
|
||||
});
|
||||
|
||||
return refreshPromise;
|
||||
};
|
||||
|
||||
return {login, logout, getAccessToken, hasAccessToken, refreshAccessToken}
|
||||
});
|
||||
|
||||
@@ -57,11 +57,11 @@ export const query = ({isQueryRunning, query, variables, refreshUnauthorized}) =
|
||||
});
|
||||
}
|
||||
|
||||
export const mutate = ({isMutationRunning, mutation, variables}) => {
|
||||
export const mutate = ({isMutationRunning, mutation, variables, refreshUnauthorized}) => {
|
||||
isMutationRunning.value = true;
|
||||
let mutationName = getOperationName(mutation);
|
||||
|
||||
// TODO: pridat refresh tokenu!
|
||||
mutation._refreshUnauthorizedUser = typeof refreshUnauthorized === 'undefined' ? true : refreshUnauthorized;
|
||||
|
||||
return apolloWrapper.mutate(mutation, variables)
|
||||
.then(result => {
|
||||
|
||||
Reference in New Issue
Block a user