@ -1,111 +0,0 @@ | |||
import jwtDecode from "jwt-decode"; | |||
import router from "./routes"; | |||
export interface IUser { | |||
id: number, | |||
name: string, | |||
isAdmin: boolean | |||
} | |||
/** | |||
* The active JWT | |||
*/ | |||
let token: string | null; | |||
/** | |||
* The decoded user object | |||
*/ | |||
let user: IUser | null; | |||
/** | |||
* Check if the user is an admin | |||
*/ | |||
export function isAdmin() { | |||
return user && user.isAdmin; | |||
} | |||
/** | |||
* Check if the client is authenticated | |||
*/ | |||
export function isAuthenticated() { | |||
return Boolean(token); | |||
} | |||
/** | |||
* Get the logged in user (assumes authentication has been checked) | |||
*/ | |||
export function getUser() { | |||
return <IUser>user; | |||
} | |||
/** | |||
* Load the token from local storage | |||
*/ | |||
export function loadToken() { | |||
try { | |||
token = localStorage.getItem("jwt"); | |||
user = jwtDecode(<string>token); | |||
} catch(e) { | |||
console.log("Failed to load token"); | |||
token = null; | |||
user = null; | |||
return false; | |||
} | |||
console.log("Token loaded", token); | |||
return true; | |||
} | |||
/** | |||
* Delete the token from local storage | |||
*/ | |||
export function forgetToken() { | |||
token = null; | |||
user = null; | |||
localStorage.removeItem("jwt"); | |||
} | |||
/** | |||
* Store a JWT token in local storage | |||
*/ | |||
export function storeToken(jwtToken: string) { | |||
try { | |||
user = jwtDecode(jwtToken); | |||
token = jwtToken; | |||
localStorage.setItem("jwt", jwtToken); | |||
} catch(e) { | |||
user = null; | |||
token = null; | |||
return false; | |||
} | |||
return true; | |||
} | |||
/** | |||
* Fetch request providing authentication and logout upon unauthorized requests | |||
*/ | |||
export async function authFetch(path: string, options: RequestInit = {}): Promise<Response> { | |||
console.log("Performing auth fetch"); | |||
options.credentials = "include"; | |||
options.headers = Object.assign(options.headers ?? {}, { | |||
"Authorization": `Bearer ${token}` | |||
}); | |||
let response = await fetch(path, options); | |||
if (response.status === 401) { | |||
// forgetToken(); | |||
console.log("Forgetting token..."); | |||
router.push({ name: "Login" }); | |||
throw Error("Unauthorized"); | |||
} | |||
return response; | |||
} | |||
/** | |||
* @TODO Remove later | |||
*/ | |||
(<any>window).forgetToken = forgetToken; | |||
(<any>window).authFetch = authFetch; | |||
/** | |||
* Ensure the token is loaded upon startup | |||
*/ | |||
loadToken(); |
@ -0,0 +1,9 @@ | |||
import { ComponentCustomProperties } from 'vue' | |||
import type { Store } from "./store"; | |||
declare module '@vue/runtime-core' { | |||
// provide typings for `this.$store` | |||
interface ComponentCustomProperties { | |||
$store: Store | |||
} | |||
} |
@ -0,0 +1,122 @@ | |||
import { ActionTree } from "vuex"; | |||
import { Actions } from "./generics"; | |||
import { IState } from "./state"; | |||
import { Mutation, MutationsTypes } from "./mutations"; | |||
import router from "../routes"; | |||
import { GettersTypes } from "./getters"; | |||
// Payload types | |||
type IAuthenticatePayload = { email: string, password: string, remember: boolean }; | |||
type IAuthenticateResult = { email?: string[], password?: string[] } | null; | |||
type IAuthFetchPayload = { path: string, options?: RequestInit }; | |||
/** | |||
* The available actions te perform | |||
*/ | |||
export enum Action { | |||
// Authentication | |||
AuthAuthenticate = "AUTH_AUTHENTICATE", | |||
AuthForget = "AUHT_FORGET", | |||
AuthLoad = "AUTH_LOAD", | |||
// RESTful | |||
AuthFetch = "AUTH_FETCH" | |||
} | |||
/** | |||
* The action function signatures | |||
*/ | |||
export type ActionsTypes = { | |||
// Authentication | |||
[Action.AuthAuthenticate]: (payload: IAuthenticatePayload) => Promise<IAuthenticateResult>, | |||
[Action.AuthForget] : () => void, | |||
[Action.AuthLoad] : () => boolean | |||
// RESTful | |||
[Action.AuthFetch]: (payload: IAuthFetchPayload) => Promise<Response> | |||
} | |||
/** | |||
* The action function implementations | |||
*/ | |||
export const actions: Actions<IState, GettersTypes, MutationsTypes, ActionsTypes> & ActionTree<IState, IState> = { | |||
// Authentication ------------------------------------------------------------------------------ | |||
/** | |||
* Authenticate the credentials of a user and log them in | |||
*/ | |||
[Action.AuthAuthenticate]({commit, dispatch}, {email, password, remember = false}) { | |||
return new Promise((resolve, reject) => { | |||
fetch(`/auth/login?use_cookies=${navigator.cookieEnabled}`, { | |||
method: "post", | |||
headers: { "Content-Type": "application/json" }, | |||
body: JSON.stringify({email, password}) | |||
}) | |||
.then(async response => { | |||
let body = await response.json(); | |||
if (response.status !== 200) { | |||
if (response.status === 401) { | |||
body.errors = { "email": [ "Email or password is incorrect" ] }; | |||
} | |||
resolve(body.errors || {}); | |||
return; | |||
} | |||
commit(Mutation.LoadUser, body.token); | |||
commit(Mutation.StoreUser, remember); | |||
resolve(null); | |||
}) | |||
.catch(e => { | |||
console.error("Error occurred during submission:", e); | |||
reject(e); | |||
}); | |||
}); | |||
}, | |||
/** | |||
* Forget the login token and log the user out | |||
*/ | |||
[Action.AuthForget]({commit}) { | |||
commit(Mutation.ForgetUser, undefined); | |||
}, | |||
/** | |||
* Load the user from local storage | |||
*/ | |||
[Action.AuthLoad]({getters, commit, dispatch}) { | |||
let token = getters.storedToken; | |||
if (!token) { | |||
return false; | |||
} | |||
try { | |||
commit(Mutation.LoadUser, token); | |||
} catch(e) { | |||
dispatch(Action.AuthForget, undefined); | |||
return false; | |||
} | |||
return true; | |||
}, | |||
// RESTful ------------------------------------------------------------------------------------- | |||
/** | |||
* Fetch request providing authentication and logout upon unauthorized requests | |||
*/ | |||
async [Action.AuthFetch]({commit, state}, {path, options = {}}) { | |||
if (state.user == null) { | |||
router.push({ name: "Login" }); | |||
throw Error("Unauthorized"); | |||
} | |||
options.credentials = "include"; | |||
options.headers = Object.assign(options.headers ?? {}, { | |||
"Authorization": `Bearer ${state.user.token}` | |||
}); | |||
let response = await fetch(path, options); | |||
if (response.status === 401) { | |||
commit(Mutation.ForgetUser, undefined); | |||
router.push({ name: "Login" }); | |||
throw Error("Unauthorized"); | |||
} | |||
return response; | |||
} | |||
}; |
@ -0,0 +1,36 @@ | |||
import { ActionContext, CommitOptions, DispatchOptions, GetterTree, MutationTree, Store as VuexStore } from "vuex"; | |||
type AugmentedActionContext<S, G extends GetterTree<S, S>, M extends MutationTree<S>, A extends ActionsType> = Omit<ActionContext<S, S>, "commit" | "dispatch" | "getters"> & { | |||
commit<K extends keyof M>(key: K, payload: Parameters<M[K]>[1]): ReturnType<M[K]>, | |||
dispatch<K extends keyof A, P extends Parameters<A[K]>[0]>( | |||
key: K, | |||
payload: P, | |||
options?: DispatchOptions | |||
): ReturnType<A[K]>, | |||
getters: { [K in keyof G]: ReturnType<G[K]> } | |||
}; | |||
type ActionsType = { | |||
[key: string]: (payload?: any) => any | |||
}; | |||
export type Actions<S, G extends GetterTree<S, S>, M extends MutationTree<S>, A extends ActionsType> = { | |||
[K in keyof A]: (context: AugmentedActionContext<S, G, M, A>, payload: Parameters<A[K]>[0]) => ReturnType<A[K]> | |||
} | |||
// type InferActionTypes<A> = A extends { [key: string]: (context: any, payload: any) => any } ? { | |||
// [K in keyof A]: (payload: Parameters<A[K]>[1]) => ReturnType<A[K]> | |||
// } : never; | |||
export type GenericStore<S, G extends GetterTree<S, S>, M extends MutationTree<S>, A extends ActionsType> = Omit<VuexStore<S>, "commit" | "getters" | "dispatch"> & { | |||
commit<K extends keyof M, P extends Parameters<M[K]>[1]>( | |||
key: K, | |||
payload: P, | |||
options?: CommitOptions | |||
): ReturnType<M[K]>, | |||
getters: { [K in keyof G]: ReturnType<G[K]> }, | |||
dispatch<K extends keyof A, P extends Parameters<A[K]>[0]>( | |||
key: K, | |||
payload: P, | |||
options?: DispatchOptions | |||
): ReturnType<A[K]> | |||
}; |
@ -0,0 +1,31 @@ | |||
import { GetterTree } from "vuex"; | |||
import { IState } from "./state"; | |||
export type GettersTypes = { | |||
isAuthenticated(state: IState): boolean, | |||
storedToken(): string | null, | |||
token(state: IState): string | null | |||
} | |||
export const getters: GettersTypes & GetterTree<IState, IState> = { | |||
/** | |||
* Determine if the current user is authenticated | |||
*/ | |||
isAuthenticated(state: IState) { | |||
return state.user != null; | |||
}, | |||
/** | |||
* Retrieve the stored token | |||
*/ | |||
storedToken() { | |||
return sessionStorage.getItem("jwt") ?? localStorage.getItem("jwt"); | |||
}, | |||
/** | |||
* Get the current token | |||
*/ | |||
token(state: IState) { | |||
return state.user?.token ?? null; | |||
} | |||
}; |
@ -0,0 +1,36 @@ | |||
import { createLogger, createStore } from "vuex"; | |||
import { GenericStore } from "./generics"; | |||
import { getters, GettersTypes } from "./getters"; | |||
// Import store information | |||
import { state, IState } from "./state"; | |||
import { mutations, MutationsTypes } from "./mutations"; | |||
import { actions, ActionsTypes } from "./actions"; | |||
// Export store keys | |||
export { Mutation } from "./mutations"; | |||
export { Action } from "./actions"; | |||
/** | |||
* The the store type | |||
*/ | |||
export type Store = GenericStore<IState, GettersTypes, MutationsTypes, ActionsTypes>; | |||
/** | |||
* Create the store instance | |||
*/ | |||
const store = createStore({ | |||
state, getters, mutations, actions, plugins: [createLogger()] | |||
}) as Store; | |||
/** | |||
* Use the store | |||
*/ | |||
export function useStore() { | |||
return store; | |||
} | |||
/** | |||
* Export the store instance by default | |||
*/ | |||
export default store; |
@ -0,0 +1,69 @@ | |||
import jwtDecode from "jwt-decode"; | |||
import { MutationTree } from "vuex"; | |||
import { IState } from "./state"; | |||
/** | |||
* All available mutation types | |||
*/ | |||
export enum Mutation { | |||
ForgetUser = "FORGET_USER", | |||
LoadUser = "LOAD_USER", | |||
StoreUser = "STORE_USER" | |||
} | |||
/** | |||
* Type declarations for each mutation | |||
*/ | |||
export type MutationsTypes<S = IState> = { | |||
[Mutation.ForgetUser]: (state: S) => void, | |||
[Mutation.LoadUser] : (state: S, token: string) => boolean, | |||
[Mutation.StoreUser] : (state: S, remember: boolean) => void, | |||
} | |||
/** | |||
* The auth mutations | |||
*/ | |||
export const mutations: MutationsTypes & MutationTree<IState> = { | |||
/** | |||
* Log the user out and remove the token from storage | |||
*/ | |||
[Mutation.ForgetUser](state: IState) { | |||
state.user = null; | |||
sessionStorage.removeItem("jwt"); | |||
localStorage.removeItem("jwt"); | |||
}, | |||
/** | |||
* Try loading a user from the provided token | |||
*/ | |||
[Mutation.LoadUser](state: IState, token: string) { | |||
try { | |||
let user: any = jwtDecode(token); | |||
state.user = { | |||
id : user.id, | |||
name : user.name, | |||
isAdmin: false, | |||
token : token | |||
} | |||
} catch(e) { | |||
return false; | |||
} | |||
return true; | |||
}, | |||
/** | |||
* Store the user to remember them | |||
*/ | |||
[Mutation.StoreUser](state: IState, remember: boolean) { | |||
if (state.user == null) { | |||
sessionStorage.removeItem("jwt"); | |||
localStorage.removeItem("jwt"); | |||
return; | |||
} | |||
if (remember) { | |||
localStorage.setItem("jwt", state.user.token); | |||
} else { | |||
sessionStorage.setItem("jwt", state.user.token); | |||
} | |||
} | |||
} |
@ -0,0 +1,6 @@ | |||
export interface IUser { | |||
id : number, | |||
name : string, | |||
isAdmin: boolean, | |||
token : string | |||
} |
@ -0,0 +1,15 @@ | |||
import { IUser } from "./schema"; | |||
/** | |||
* The state definition | |||
*/ | |||
export interface IState { | |||
user: IUser | null, | |||
} | |||
/** | |||
* The state implementation | |||
*/ | |||
export const state: IState = { | |||
user: null | |||
}; |