import { ActionContext, ActionTree, GetterTree, MutationTree, Module, Store as VuexStore, CommitOptions, DispatchOptions, } from 'vuex' import { SignUpParams } from '@aws-amplify/auth/lib-esm/types' import { CognitoUserAttribute as Attribute } from 'amazon-cognito-identity-js' import Auth from '@aws-amplify/auth' import { State as RootState } from '@/store' import { AccountService } from '@/modules/auth/services/account' // Declare types export type AuthenticationStatus = { state?: string message?: string variant?: string } export type Credentials = { username: string password: string } export type ConfirmationParams = { username: string code: string } export enum AttributeNames { NAME = 'name', EMAIL = 'email', EMAIL_VERIFIED = 'email_verified', SUB = 'sub', } // Declare state export type State = { teamId?: string isAuthenticated: boolean authenticationStatus?: AuthenticationStatus passwordForgetUsername?: string attributes?: Attribute[] } // Create initial state const state: State = { isAuthenticated: false, } // mutations enums export enum MutationTypes { AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR', CLEAR_AUTHENTICATION_STATUS = 'CLEAR_AUTHENTICATION_STATUS', CLEAR_AUTHENTICATION = 'CLEAR_AUTHENTICATION', SET_USER_AUTHENTICATED = 'SET_USER_AUTHENTICATED', SET_TEAM_ID = 'SET_TEAM_ID', PASSWORD_FORGET_SET_USERNAME = 'PASSWORD_FORGET_SET_USERNAME', CLEAR_PASSWORD_FORGET_USERNAME = 'CLEAR_PASSWORD_FORGET_SET_USERNAME', SET_ATTRIBUTES = 'SET_ATTRIBUTES', } // Mutation contracts export type Mutations = { [MutationTypes.AUTHENTICATION_ERROR](state: S, err: Error): void [MutationTypes.CLEAR_AUTHENTICATION_STATUS](state: S): void [MutationTypes.CLEAR_AUTHENTICATION](state: S): void [MutationTypes.SET_USER_AUTHENTICATED](state: S): void [MutationTypes.SET_TEAM_ID](state: S, teamId: string): void [MutationTypes.PASSWORD_FORGET_SET_USERNAME](state: S, username: string): void [MutationTypes.CLEAR_PASSWORD_FORGET_USERNAME](state: S): void [MutationTypes.SET_ATTRIBUTES](state: S, attributes: Attribute[]): void } // Define mutations const mutations: MutationTree & Mutations = { [MutationTypes.AUTHENTICATION_ERROR](state: State, err: Error) { state.isAuthenticated = false state.authenticationStatus = { state: 'failed', message: err.message, variant: 'danger', } }, [MutationTypes.CLEAR_AUTHENTICATION_STATUS](state: State) { state.authenticationStatus = undefined }, [MutationTypes.CLEAR_AUTHENTICATION](state: State) { state.isAuthenticated = false state.authenticationStatus = undefined }, [MutationTypes.SET_USER_AUTHENTICATED](state: State) { state.isAuthenticated = true }, [MutationTypes.SET_TEAM_ID](state: State, teamId: string) { state.teamId = teamId }, [MutationTypes.PASSWORD_FORGET_SET_USERNAME](state: State, username: string) { state.passwordForgetUsername = username }, [MutationTypes.CLEAR_PASSWORD_FORGET_USERNAME](state: State) { state.passwordForgetUsername = undefined }, [MutationTypes.SET_ATTRIBUTES](state: State, attributes: Attribute[]) { state.attributes = attributes }, } // Action enums export enum ActionTypes { SIGNUP = 'SIGNUP', CONFIRM_SIGNUP = 'CONFIRM_SIGNUP', SIGNIN = 'SIGNIN', SIGNOUT = 'SIGNOUT', INIT_PASSWORD_FORGET = 'INIT_PASSWORD_FORGET', PASSWORD_FORGET_SUBMIT = 'PASSWORD_FORGET_SUBMIT', FETCH_ATTRIBUTES = 'FETCH_ATTRIBUTES', CHANGE_EMAIL = 'CHANGE_EMAIL', } // Actions context type AugmentedActionContext = { commit( key: K, payload: Parameters[1], ): ReturnType getters( key: K, payload: Parameters[1], ): ReturnType } & Omit, 'commit'> // Actions contracts export interface Actions { [ActionTypes.SIGNUP]( { commit }: AugmentedActionContext, payload: SignUpParams, ): void [ActionTypes.CONFIRM_SIGNUP]( { commit }: AugmentedActionContext, payload: ConfirmationParams, ): void [ActionTypes.SIGNIN]( { commit }: AugmentedActionContext, payload: Credentials, ): void [ActionTypes.SIGNOUT]( { commit }: AugmentedActionContext, payload: undefined, ): void [ActionTypes.INIT_PASSWORD_FORGET]( { commit }: AugmentedActionContext, username: string, ): void [ActionTypes.PASSWORD_FORGET_SUBMIT]( { commit }: AugmentedActionContext, payload: { username: string; code: string; password: string }, ): void [ActionTypes.FETCH_ATTRIBUTES]( { commit }: AugmentedActionContext, payload: undefined, ): void [ActionTypes.CHANGE_EMAIL]( { commit, getters }: AugmentedActionContext, email: string, ): void } // Define actions export const actions: ActionTree & Actions = { async [ActionTypes.SIGNUP]({ commit }, payload: SignUpParams) { commit(MutationTypes.CLEAR_AUTHENTICATION_STATUS, undefined) try { await Auth.signUp(payload) commit(MutationTypes.CLEAR_AUTHENTICATION, undefined) } catch (err) { commit(MutationTypes.AUTHENTICATION_ERROR, err) } }, async [ActionTypes.CONFIRM_SIGNUP]({ commit }, payload: ConfirmationParams) { commit(MutationTypes.CLEAR_AUTHENTICATION_STATUS, undefined) try { await Auth.confirmSignUp(payload.username, payload.code) } catch (err) { commit(MutationTypes.AUTHENTICATION_ERROR, err) } }, async [ActionTypes.SIGNIN]({ commit }, payload: Credentials) { commit(MutationTypes.CLEAR_AUTHENTICATION_STATUS, undefined) try { await Auth.signIn(payload.username, payload.password) commit(MutationTypes.SET_USER_AUTHENTICATED, undefined) // toDo: Temporary solution for setting team id const accountService = new AccountService() const account = await accountService.getAccount() commit(MutationTypes.SET_TEAM_ID, account.teams[0].team_id) } catch (err) { commit(MutationTypes.AUTHENTICATION_ERROR, err) } }, async [ActionTypes.SIGNOUT]({ commit }) { try { await Auth.signOut() } catch (err) { commit(MutationTypes.AUTHENTICATION_ERROR, err) } commit(MutationTypes.CLEAR_AUTHENTICATION, undefined) }, async [ActionTypes.INIT_PASSWORD_FORGET]({ commit }, username: string) { commit(MutationTypes.CLEAR_AUTHENTICATION_STATUS, undefined) try { await Auth.forgotPassword(username) } catch (err) { commit(MutationTypes.AUTHENTICATION_ERROR, err) } }, async [ActionTypes.PASSWORD_FORGET_SUBMIT]( { commit }, payload: { username: string; code: string; password: string }, ) { const { username, code, password } = payload commit(MutationTypes.CLEAR_AUTHENTICATION_STATUS, undefined) try { await Auth.forgotPasswordSubmit(username, code, password) } catch (err) { commit(MutationTypes.AUTHENTICATION_ERROR, err) } }, async [ActionTypes.FETCH_ATTRIBUTES]({ commit }) { const user = await Auth.currentUserPoolUser() try { const attributes: Attribute[] = await Auth.userAttributes(user) commit(MutationTypes.SET_ATTRIBUTES, attributes) } catch (err) { commit(MutationTypes.AUTHENTICATION_ERROR, err) } }, async [ActionTypes.CHANGE_EMAIL]({ commit, getters }, email) { const emailAttribute = getters.getAttribute(AttributeNames.EMAIL) try { const user = await Auth.currentUserPoolUser() await Auth.updateUserAttributes(user, { ...emailAttribute, Value: email }) } catch (err) { commit(MutationTypes.AUTHENTICATION_ERROR, err) } }, } // getters types export type Getters = { isAuthenticated(state: State): boolean hasAuthenticationStatus(state: State): boolean getAuthenticationStatus(state: State): AuthenticationStatus | undefined getTeamId(state: State): string | undefined getPasswordForgetUsername(state: State): string | undefined getAttributes(state: State): Attribute[] | undefined getAttribute(state: State): (name: AttributeNames) => Attribute | undefined getFullName(state: State, getters: Getters): string | undefined getEmail(state: State, getters: Getters): string | undefined isEmailVerified(state: State, getters: Getters): string | undefined } // getters export const getters: GetterTree & Getters = { isAuthenticated: (state) => state.isAuthenticated, hasAuthenticationStatus: (state) => !!state.authenticationStatus, getAuthenticationStatus: (state) => state.authenticationStatus, getTeamId: (state) => state.teamId, getPasswordForgetUsername: (state) => state.passwordForgetUsername, getAttributes: (state) => state.attributes, getAttribute: (state) => (name: AttributeNames) => { if (state.attributes === undefined) return undefined return state.attributes.find((attr) => attr.getName() === name) }, getFullName: (state, getters) => { return getters.getAttribute(AttributeNames.NAME)?.getValue() }, getEmail: (state, getters) => { return getters.getAttribute(AttributeNames.EMAIL)?.getValue() }, isEmailVerified: (state, getters) => { return getters.getAttribute(AttributeNames.EMAIL_VERIFIED)?.getValue() }, } //setup store type export type Store = Omit< VuexStore, 'commit' | 'getters' | 'dispatch' > & { commit[1]>( key: K, payload: P, options?: CommitOptions, ): ReturnType } & { getters: { [K in keyof Getters]: ReturnType } } & { dispatch( key: K, payload: Parameters[1], options?: DispatchOptions, ): ReturnType } export const AuthModule: Module = { state, mutations, actions, // Namespacing Vuex modules is tricky and hard to type check with typescript. // Instead of namespacing, we could create our own namespacing mechanism by // prefixing the value of the TypeScript enum with the namespace, e.g. // enum TodoActions { // AddTodo = 'TODO__ADD_TODO' // } // namespaced: true, getters, }