@ -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 | |||||
}; |