I am currently using Vue 3 along with the latest Quasar Framework.
To simplify my API calls, I created an Api class as a wrapper for Axios with various methods such as get, post, etc.
Now, I need to intercept these method calls.
In order to achieve this, I created a Proxy for the Api class instance. The goal is to redirect the user to the login page if they are Unauthenticated and also retrieve CSRF cookies if required before repeating the request.
However, when trying to use the Api instance with the Proxy, I encountered an error:
async function signIn() {
loading.value = true;
const payload: object = {
email: login.value,
password: password.value,
remember: remember.value
}
try {
const response = await api.post('/login', payload);
console.log(response)
} catch (e) {
console.error(e);
}
loading.value = false;
}
The error thrown was:
LoginForm.vue?0a10:29 TypeError: boot_api__WEBPACK_IMPORTED_MODULE_1__.api.post is not a function
at eval (LoginForm.vue?0a10:26:1)
at Generator.next (<anonymous>)
at eval (VM2781 LoginForm.vue:13:71)
at new Promise (<anonymous>)
at __awaiter (VM2781 LoginForm.vue:9:12)
at signIn (LoginForm.vue?0a10:17:1)
at callWithErrorHandling (runtime-core.esm-bundler.js?f781:155:1)
at callWithAsyncErrorHandling (runtime-core.esm-bundler.js?f781:164:1)
at emit$1 (runtime-core.esm-bundler.js?f781:720:1)
at eval (runtime-core.esm-bundler.js?f781:7292:1)
I'm puzzled by this issue. My IDE shows no errors, and everything works fine without using the Proxy...
Here's the complete code for the LoginForm vue component:
<template>
<q-form
@submit="signIn"
class="column q-gutter-y-md login-form-width"
>
<q-input v-model="login"
label="Login"
class="col"
filled
:disable="loading"
/>
<q-input v-model="password"
label="Password"
type="password"
class="col"
filled
:disable="loading"
/>
<q-checkbox v-model="remember"
label="Remember"
class="col"
filled
:disable="loading"
/>
<q-btn label="Login"
type="submit"
unelevated
color="primary"
:loading="loading"
/>
</q-form>
</template>
<script lang="ts">
import {defineComponent, Ref, ref} from 'vue';
import {api} from 'boot/api';
export default defineComponent({
name: 'LoginForm',
setup() {
const login: Ref<string> = ref('');
const password: Ref<string> = ref('');
const remember: Ref<boolean> = ref(true);
const loading: Ref<boolean> = ref(false);
const errors: Ref<object> = ref({
login: [],
password: [],
});
async function signIn() {
loading.value = true;
const payload: object = {
email: login.value,
password: password.value,
remember: remember.value
}
try {
const response = await api.post('/login', payload);
console.log(response)
} catch (e) {
console.error(e);
}
loading.value = false;
}
return {login, password, remember, loading, errors, signIn}
}
})
</script>
<style scoped>
.login-form-width {
width: 100%;
max-width: 350px;
}
</style>
And here's the full code for the api (from the Quasar boot file):
import {boot} from 'quasar/wrappers'
import {AxiosError, AxiosInstance, Method} from 'axios';
import {axiosInstance} from 'boot/axios';
type ApiMethod = 'get' | 'post';
class ApiResponse {
public data: object
public code: number | null
public message: string | null
constructor(data: object = {}, code: number | null = 0, message: string | null = '') {
this.data = data;
this.code = code;
this.message = message;
}
}
class ApiError {
public data: object
public code: number | null
public message: string | null
constructor(data: object = {}, code: number | null = 0, message: string | null = '') {
this.data = data;
this.code = code;
this.message = message;
}
}
interface ApiRequestConfig {
url: string,
method: Method,
headers?: object,
params?: object,
data?: object | string,
}
class Api {
axios: AxiosInstance
constructor(axios: AxiosInstance) {
this.axios = axios;
this.axios.defaults.withCredentials = true;
}
public async get(url: string, params: object = {}): Promise<ApiResponse> {
return this.request({method: 'GET', url, params});
}
public async post(url: string, payload: object = {}): Promise<ApiResponse> {
return this.request({method: 'POST', url, data: payload});
}
private async request(config: ApiRequestConfig): Promise<ApiResponse> {
try {
const response = await this.axios.request(config);
return new ApiResponse(response.data, response.status, response.statusText)
} catch (e) {
const error = e as AxiosError;
if (error.response) {
throw new ApiError(error.response.data, error.response.status, error.response.statusText);
} else {
throw new ApiError({}, 0, '')
}
}
}
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$api: Api;
}
}
let api: Api = new Api(axiosInstance);
// "async" is optional;
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
export default boot(async ({router}) => {
api = new Proxy(api, {
async get(target: Api, prop: ApiMethod) {
if (typeof target[prop] === 'function') {
return async function func(args: unknown[]): Promise<unknown> {
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return await target[prop](...args);
} catch (e) {
const error = e as ApiError;
if (error.code === 401 || error.code === 403) {
await router.push('login');
}
if (error.code === 419) {
await target.get('/sanctum/csrf-cookie');
return func(args);
}
}
}
}
}
})
})
export {api};