2021-01-21 11:36:18 +08:00
|
|
|
import { Context } from '@nuxt/types';
|
|
|
|
import { Inject } from '@nuxt/types/app';
|
2021-02-04 17:34:24 +08:00
|
|
|
import { AxiosError, AxiosRequestConfig } from 'axios';
|
2021-04-05 11:17:34 +08:00
|
|
|
import Cookie from 'cookie';
|
2021-02-04 17:34:24 +08:00
|
|
|
import jwtDecode, { JwtPayload } from 'jwt-decode';
|
2021-02-05 08:22:22 +08:00
|
|
|
import qs from 'qs';
|
2021-04-03 14:56:55 +08:00
|
|
|
import { HangarApiException } from 'hangar-api';
|
2021-02-04 17:34:24 +08:00
|
|
|
|
2021-04-05 11:17:34 +08:00
|
|
|
interface StatCookie {
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
hangar_stats: string;
|
|
|
|
Path: string;
|
|
|
|
'Max-Age': string;
|
|
|
|
Expires: string;
|
|
|
|
SameSite: Cookie.CookieSerializeOptions['sameSite'];
|
|
|
|
Secure?: boolean;
|
|
|
|
}
|
|
|
|
|
2021-02-09 02:26:18 +08:00
|
|
|
const createApi = ({ $axios, store, app: { $cookies } }: Context) => {
|
2021-01-21 11:36:18 +08:00
|
|
|
class API {
|
2021-02-04 17:34:24 +08:00
|
|
|
getToken(forceFetch: boolean = true): Promise<string | null> {
|
|
|
|
if (store.state.auth.token) {
|
|
|
|
if (API.validateToken(store.state.auth.token)) {
|
|
|
|
return Promise.resolve(store.state.auth.token);
|
2021-01-21 11:36:18 +08:00
|
|
|
} else {
|
2021-02-04 17:34:24 +08:00
|
|
|
return this.refreshToken();
|
2021-01-21 11:36:18 +08:00
|
|
|
}
|
2021-02-04 17:34:24 +08:00
|
|
|
} else if (forceFetch) {
|
2021-02-03 07:47:15 +08:00
|
|
|
return this.refreshToken();
|
|
|
|
} else {
|
|
|
|
return Promise.resolve(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-04 17:34:24 +08:00
|
|
|
private refreshToken(): Promise<string | null> {
|
2021-02-03 07:47:15 +08:00
|
|
|
return new Promise<string | null>((resolve) => {
|
|
|
|
return $axios
|
2021-02-05 02:11:03 +08:00
|
|
|
.get<{ token: string; refreshToken: string; cookieName: string; expiresIn: number }>('/refresh')
|
2021-02-03 07:47:15 +08:00
|
|
|
.then((value) => {
|
2021-02-04 17:34:24 +08:00
|
|
|
store.commit('auth/SET_TOKEN', value.data.token);
|
2021-02-05 01:49:02 +08:00
|
|
|
$cookies.set(value.data.cookieName, value.data.refreshToken, {
|
2021-02-04 17:34:24 +08:00
|
|
|
path: '/',
|
2021-02-05 02:11:03 +08:00
|
|
|
expires: new Date(Date.now() + value.data.expiresIn * 1000),
|
2021-02-04 17:34:24 +08:00
|
|
|
sameSite: 'strict',
|
2021-03-17 01:43:58 +08:00
|
|
|
secure: process.env.nodeEnv === 'production',
|
2021-02-04 17:34:24 +08:00
|
|
|
});
|
|
|
|
resolve(value.data.token);
|
2021-02-03 07:47:15 +08:00
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
resolve(null);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-02-04 17:34:24 +08:00
|
|
|
request<T>(url: string, authed: boolean = true, method: AxiosRequestConfig['method'] = 'get', data: object = {}): Promise<T> {
|
|
|
|
return this.getToken(authed).then((token) => {
|
|
|
|
return this._request(`v1/${url}`, token, method, data);
|
2021-01-22 13:18:09 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-02-09 02:26:18 +08:00
|
|
|
requestInternal<T = void>(url: string, authed: boolean = true, method: AxiosRequestConfig['method'] = 'get', data: object = {}): Promise<T> {
|
2021-02-04 17:34:24 +08:00
|
|
|
return this.getToken(authed).then((token) => {
|
|
|
|
return this.requestInternalWithToken<T>(url, token, authed, method, data);
|
2021-02-03 07:47:15 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-02-04 17:34:24 +08:00
|
|
|
requestInternalWithToken<T>(
|
|
|
|
url: string,
|
|
|
|
token: string | null,
|
|
|
|
authed: boolean = true,
|
|
|
|
method: AxiosRequestConfig['method'] = 'get',
|
|
|
|
data: object = {}
|
|
|
|
): Promise<T> {
|
2021-02-08 07:19:16 +08:00
|
|
|
let tokenPromise: Promise<string | null>;
|
2021-02-04 17:34:24 +08:00
|
|
|
if (token && !API.validateToken(token)) {
|
2021-02-08 07:19:16 +08:00
|
|
|
tokenPromise = this.getToken(true);
|
2021-02-04 17:34:24 +08:00
|
|
|
} else {
|
2021-02-08 07:19:16 +08:00
|
|
|
tokenPromise = Promise.resolve(token);
|
2021-02-04 17:34:24 +08:00
|
|
|
}
|
|
|
|
|
2021-02-08 07:19:16 +08:00
|
|
|
return tokenPromise.then((token) => {
|
2021-02-04 17:34:24 +08:00
|
|
|
if (authed && !token) {
|
2021-04-03 14:56:55 +08:00
|
|
|
// eslint-disable-next-line prefer-promise-reject-errors
|
|
|
|
return Promise.reject({
|
|
|
|
isAxiosError: true,
|
|
|
|
response: {
|
|
|
|
data: {
|
|
|
|
isHangarApiException: true,
|
|
|
|
httpError: {
|
|
|
|
statusCode: 401,
|
|
|
|
},
|
|
|
|
message: 'You must be logged in',
|
|
|
|
} as HangarApiException,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return this._request(`internal/${url}`, token, method, data);
|
2021-02-04 17:34:24 +08:00
|
|
|
}
|
2021-02-03 07:47:15 +08:00
|
|
|
});
|
2021-01-21 11:36:18 +08:00
|
|
|
}
|
|
|
|
|
2021-02-04 17:34:24 +08:00
|
|
|
private static validateToken(token: string): boolean {
|
|
|
|
const decodedToken = jwtDecode<JwtPayload>(token);
|
|
|
|
if (!decodedToken.exp) {
|
|
|
|
return false;
|
|
|
|
}
|
2021-02-05 02:11:03 +08:00
|
|
|
return decodedToken.exp * 1000 > Date.now() - 10 * 1000; // check against 10 seconds earlier to mitigate tokens expiring mid-request
|
2021-01-22 13:18:09 +08:00
|
|
|
}
|
|
|
|
|
2021-02-04 17:34:24 +08:00
|
|
|
private _request<T>(url: string, token: string | null, method: AxiosRequestConfig['method'], data: object): Promise<T> {
|
2021-02-10 16:31:43 +08:00
|
|
|
const headers: Record<string, string> = token ? { Authorization: `HangarAuth ${token}` } : {};
|
2021-01-21 11:36:18 +08:00
|
|
|
return new Promise<T>((resolve, reject) => {
|
2021-02-04 17:34:24 +08:00
|
|
|
return $axios
|
|
|
|
.request<T>({
|
|
|
|
method,
|
|
|
|
url: `/api/${url}`,
|
|
|
|
headers,
|
2021-02-05 08:22:22 +08:00
|
|
|
data: method?.toLowerCase() !== 'get' ? data : {},
|
|
|
|
params: method?.toLowerCase() === 'get' ? data : {},
|
|
|
|
paramsSerializer: (params) => {
|
|
|
|
return qs.stringify(params, {
|
|
|
|
arrayFormat: 'repeat',
|
|
|
|
});
|
|
|
|
},
|
2021-01-22 11:11:24 +08:00
|
|
|
})
|
2021-04-05 11:17:34 +08:00
|
|
|
.then(({ data, headers }) => {
|
|
|
|
// check for stats cookie
|
|
|
|
if (headers['set-cookie']) {
|
|
|
|
const statString = headers['set-cookie'].find((c: string) => c.startsWith('hangar_stats'));
|
|
|
|
if (statString) {
|
|
|
|
const statCookie: StatCookie = (Cookie.parse(statString) as unknown) as StatCookie;
|
|
|
|
$cookies.set('hangar_stats', statCookie.hangar_stats, {
|
|
|
|
path: statCookie.Path,
|
|
|
|
expires: new Date(statCookie.Expires),
|
|
|
|
sameSite: 'strict',
|
|
|
|
secure: statCookie.Secure,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
resolve(data);
|
|
|
|
})
|
2021-02-04 17:34:24 +08:00
|
|
|
.catch((error: AxiosError) => {
|
|
|
|
reject(error);
|
2021-01-22 15:27:42 +08:00
|
|
|
});
|
2021-01-21 11:36:18 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new API();
|
|
|
|
};
|
|
|
|
|
|
|
|
type apiType = ReturnType<typeof createApi>;
|
|
|
|
|
|
|
|
declare module 'vue/types/vue' {
|
|
|
|
interface Vue {
|
|
|
|
$api: apiType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
declare module '@nuxt/types' {
|
|
|
|
interface NuxtAppOptions {
|
|
|
|
$api: apiType;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Context {
|
|
|
|
$api: apiType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
declare module 'vuex/types/index' {
|
2021-01-31 10:00:11 +08:00
|
|
|
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars
|
2021-01-21 11:36:18 +08:00
|
|
|
interface Store<S> {
|
|
|
|
$api: apiType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-04 17:34:24 +08:00
|
|
|
export default (ctx: Context, inject: Inject) => {
|
|
|
|
inject('api', createApi(ctx));
|
2021-01-21 11:36:18 +08:00
|
|
|
};
|