first commit

This commit is contained in:
Akhil Gupta
2021-05-29 15:20:50 +05:30
commit d25c30a7b2
194 changed files with 49873 additions and 0 deletions

10
ui/src/state/helpers.js Normal file
View File

@@ -0,0 +1,10 @@
import { mapState, mapGetters, mapActions } from 'vuex'
export const authComputed = {
...mapState('auth', {
currentUser: (state) => state.currentUser,
}),
...mapGetters('auth', ['loggedIn']),
}
export const authMethods = mapActions('auth', ['logIn', 'logOut'])

View File

@@ -0,0 +1,95 @@
import axios from 'axios'
export const state = {
currentUser: getSavedState('auth.currentUser'),
initialized: getSavedState('system.initialized'),
}
export const mutations = {
SET_CURRENT_USER(state, newValue) {
state.currentUser = newValue
saveState('auth.currentUser', newValue)
setDefaultAuthHeaders(state)
},
SET_INITIALIZATION_STATUS(state, newValue) {
state.initialized = newValue
saveState('system.initialized', newValue)
},
}
export const getters = {
// Whether the user is currently logged in.
loggedIn(state) {
return !!state.currentUser
},
isInitialized(state) {
return state.initialized == null || state.initialized.initialized
},
}
export const actions = {
// This is automatically run in `src/state/store.js` when the app
// starts, along with any other actions named `init` in other modules.
init({ state, dispatch }) {
dispatch('systemInitialized')
setDefaultAuthHeaders(state)
dispatch('validate')
},
logIn({ commit, dispatch, getters }, { username, password } = {}) {
if (getters.loggedIn) return dispatch('validate')
return axios.post('/api/login', { email: username, password }).then((response) => {
const user = response.data
commit('SET_CURRENT_USER', user)
dispatch('vehicles/fetchMasters', null, { root: true })
return user
})
},
// Logs out the current user.
logOut({ commit }) {
commit('SET_CURRENT_USER', null)
},
// Validates the current user's token and refreshes it
// with new data from the API.
validate({ commit, state }) {
if (!state.currentUser) return Promise.resolve(null)
return axios
.post('/api/refresh', { refreshToken: state.currentUser.refreshToken })
.then((response) => {
const user = response.data
commit('SET_CURRENT_USER', user)
return user
})
.catch((ex) => {
commit('SET_CURRENT_USER', null)
})
},
systemInitialized({ commit, state }) {
return axios.get('/api/system/status').then((response) => {
const data = response.data
commit('SET_INITIALIZATION_STATUS', data)
return data
})
},
}
// ===
// Private helpers
// ===
function getSavedState(key) {
return JSON.parse(window.localStorage.getItem(key))
}
function saveState(key, state) {
window.localStorage.setItem(key, JSON.stringify(state))
}
function setDefaultAuthHeaders(state) {
axios.defaults.headers.common.Authorization = state.currentUser ? state.currentUser.token : ''
}

View File

@@ -0,0 +1,121 @@
import axios from 'axios'
import * as authModule from './auth'
describe('@state/modules/auth', () => {
it('exports a valid Vuex module', () => {
expect(authModule).toBeAVuexModule()
})
describe('in a store', () => {
let store
beforeEach(() => {
store = createModuleStore(authModule)
window.localStorage.clear()
})
it('mutations.SET_CURRENT_USER correctly sets axios default authorization header', () => {
axios.defaults.headers.common.Authorization = ''
store.commit('SET_CURRENT_USER', { token: 'some-token' })
expect(axios.defaults.headers.common.Authorization).toEqual('some-token')
store.commit('SET_CURRENT_USER', null)
expect(axios.defaults.headers.common.Authorization).toEqual('')
})
it('mutations.SET_CURRENT_USER correctly saves currentUser in localStorage', () => {
let savedCurrentUser = JSON.parse(
window.localStorage.getItem('auth.currentUser')
)
expect(savedCurrentUser).toEqual(null)
const expectedCurrentUser = { token: 'some-token' }
store.commit('SET_CURRENT_USER', expectedCurrentUser)
savedCurrentUser = JSON.parse(
window.localStorage.getItem('auth.currentUser')
)
expect(savedCurrentUser).toEqual(expectedCurrentUser)
})
it('getters.loggedIn returns true when currentUser is an object', () => {
store.commit('SET_CURRENT_USER', {})
expect(store.getters.loggedIn).toEqual(true)
})
it('getters.loggedIn returns false when currentUser is null', () => {
store.commit('SET_CURRENT_USER', null)
expect(store.getters.loggedIn).toEqual(false)
})
it('actions.logIn resolves to a refreshed currentUser when already logged in', () => {
expect.assertions(2)
store.commit('SET_CURRENT_USER', { token: validUserExample.token })
return store.dispatch('logIn').then((user) => {
expect(user).toEqual(validUserExample)
expect(store.state.currentUser).toEqual(validUserExample)
})
})
it('actions.logIn commits the currentUser and resolves to the user when NOT already logged in and provided a correct username and password', () => {
expect.assertions(2)
return store
.dispatch('logIn', { username: 'admin', password: 'password' })
.then((user) => {
expect(user).toEqual(validUserExample)
expect(store.state.currentUser).toEqual(validUserExample)
})
})
it('actions.logIn rejects with 401 when NOT already logged in and provided an incorrect username and password', () => {
expect.assertions(1)
return store
.dispatch('logIn', {
username: 'bad username',
password: 'bad password',
})
.catch((error) => {
expect(error.message).toEqual('Request failed with status code 401')
})
})
it('actions.validate resolves to null when currentUser is null', () => {
expect.assertions(1)
store.commit('SET_CURRENT_USER', null)
return store.dispatch('validate').then((user) => {
expect(user).toEqual(null)
})
})
it('actions.validate resolves to null when currentUser contains an invalid token', () => {
expect.assertions(2)
store.commit('SET_CURRENT_USER', { token: 'invalid-token' })
return store.dispatch('validate').then((user) => {
expect(user).toEqual(null)
expect(store.state.currentUser).toEqual(null)
})
})
it('actions.validate resolves to a user when currentUser contains a valid token', () => {
expect.assertions(2)
store.commit('SET_CURRENT_USER', { token: validUserExample.token })
return store.dispatch('validate').then((user) => {
expect(user).toEqual(validUserExample)
expect(store.state.currentUser).toEqual(validUserExample)
})
})
})
})
const validUserExample = {
id: 1,
username: 'admin',
name: 'Vue Master',
token: 'valid-token-for-admin',
}

View File

@@ -0,0 +1,81 @@
// Register each file as a corresponding Vuex module. Module nesting
// will mirror [sub-]directory hierarchy and modules are namespaced
// as the camelCase equivalent of their file name.
import camelCase from 'lodash/camelCase'
const modulesCache = {}
const storeData = { modules: {} }
;(function updateModules() {
// Allow us to dynamically require all Vuex module files.
// https://webpack.js.org/guides/dependency-management/#require-context
const requireModule = require.context(
// Search for files in the current directory.
'.',
// Search for files in subdirectories.
true,
// Include any .js files that are not this file or a unit test.
/^((?!index|\.unit\.).)*\.js$/
)
// For every Vuex module...
requireModule.keys().forEach((fileName) => {
const moduleDefinition =
requireModule(fileName).default || requireModule(fileName)
// Skip the module during hot reload if it refers to the
// same module definition as the one we have cached.
if (modulesCache[fileName] === moduleDefinition) return
// Update the module cache, for efficient hot reloading.
modulesCache[fileName] = moduleDefinition
// Get the module path as an array.
const modulePath = fileName
// Remove the "./" from the beginning.
.replace(/^\.\//, '')
// Remove the file extension from the end.
.replace(/\.\w+$/, '')
// Split nested modules into an array path.
.split(/\//)
// camelCase all module namespaces and names.
.map(camelCase)
// Get the modules object for the current path.
const { modules } = getNamespace(storeData, modulePath)
// Add the module to our modules object.
modules[modulePath.pop()] = {
// Modules are namespaced by default.
namespaced: true,
...moduleDefinition,
}
})
// If the environment supports hot reloading...
if (module.hot) {
// Whenever any Vuex module is updated...
module.hot.accept(requireModule.id, () => {
// Update `storeData.modules` with the latest definitions.
updateModules()
// Trigger a hot update in the store.
require('../store').default.hotUpdate({ modules: storeData.modules })
})
}
})()
// Recursively get the namespace of a Vuex module, even if nested.
function getNamespace(subtree, path) {
if (path.length === 1) return subtree
const namespace = path.shift()
subtree.modules[namespace] = {
modules: {},
namespaced: true,
...subtree.modules[namespace],
}
return getNamespace(subtree.modules[namespace], path)
}
export default storeData.modules

View File

@@ -0,0 +1,88 @@
import axios from 'axios'
export const state = {
cached: [],
me: null,
}
export const getters = {}
export const mutations = {
CACHE_USER(state, newUser) {
state.cached.push(newUser)
},
CACHE_MY_USER(state, newUser) {
state.me = newUser
},
}
export const actions = {
init({ dispatch, rootState }) {
const { currentUser } = rootState.auth
if (currentUser != null) {
dispatch('me')
}
},
forceMe({ commit, state }) {
return axios
.get('/api/me')
.then((response) => {
commit('CACHE_MY_USER', response.data)
return response.data
})
.catch((error) => {
if (error.response && error.response.status === 401) {
commit('CACHE_MY_USER', null)
} else {
console.warn(error)
}
return null
})
},
users() {
return axios
.get('/api/users')
.then((response) => {
return response.data
})
.catch((error) => {
if (error.response && error.response.status === 401) {
} else {
console.warn(error)
}
return null
})
},
me({ commit, state }) {
if (state.me) {
return Promise.resolve(state.me)
}
return axios
.get('/api/me')
.then((response) => {
commit('CACHE_MY_USER', response.data)
return response.data
})
.catch((error) => {
if (error.response && error.response.status === 401) {
commit('CACHE_MY_USER', null)
} else {
console.warn(error)
}
return null
})
},
fetchUser({ commit, state, rootState }, { username }) {
// 1. Check if we already have the user as a current user.
const { currentUser } = rootState.auth
if (currentUser && currentUser.username === username) {
return Promise.resolve(currentUser)
}
// 2. Check if we've already fetched and cached the user.
const matchedUser = state.cached.find((user) => user.username === username)
if (matchedUser) {
return Promise.resolve(matchedUser)
}
},
}

View File

@@ -0,0 +1,64 @@
import * as usersModule from './users'
describe('@state/modules/users', () => {
it('exports a valid Vuex module', () => {
expect(usersModule).toBeAVuexModule()
})
describe('in a store when logged in', () => {
let store
beforeEach(() => {
store = createModuleStore(usersModule, {
currentUser: validUserExample,
})
})
it('actions.fetchUser returns the current user without fetching it again', () => {
expect.assertions(2)
const axios = require('axios')
const originalAxiosGet = axios.get
axios.get = jest.fn()
return store.dispatch('fetchUser', { username: 'admin' }).then((user) => {
expect(user).toEqual(validUserExample)
expect(axios.get).not.toHaveBeenCalled()
axios.get = originalAxiosGet
})
})
it('actions.fetchUser rejects with 400 when provided a bad username', () => {
expect.assertions(1)
return store
.dispatch('fetchUser', { username: 'bad-username' })
.catch((error) => {
expect(error.response.status).toEqual(400)
})
})
})
describe('in a store when logged out', () => {
let store
beforeEach(() => {
store = createModuleStore(usersModule)
})
it('actions.fetchUser rejects with 401', () => {
expect.assertions(1)
return store
.dispatch('fetchUser', { username: 'admin' })
.catch((error) => {
expect(error.response.status).toEqual(401)
})
})
})
})
const validUserExample = {
id: 1,
username: 'admin',
name: 'Vue Master',
token: 'valid-token-for-admin',
}

View File

@@ -0,0 +1,49 @@
import axios from 'axios'
export const state = {
isMobile: false,
settings: null,
}
export const mutations = {
CACHE_ISMOBILE(state, isMobile) {
state.isMobile = isMobile
},
CACHE_SETTINGS(state, settings) {
state.settings = settings
},
}
export const getters = {}
export const actions = {
init({ dispatch, rootState }) {
dispatch('checkSize')
const { currentUser } = rootState.auth
if (currentUser) {
dispatch('getSettings')
}
},
checkSize({ commit }) {
commit('CACHE_ISMOBILE', window.innerWidth < 600)
return window.innerWidth < 600
},
getSettings({ commit }) {
return axios.get(`/api/settings`).then((response) => {
const data = response.data
commit('CACHE_SETTINGS', data)
return data
})
},
saveSettings({ commit, dispatch }, { settings }) {
return axios.post(`/api/settings`, { ...settings }).then((response) => {
const data = response.data
dispatch('getSettings')
return data
})
},
saveUserSettings({ commit, dispatch }, { settings }) {
return axios.post(`/api/me/settings`, { ...settings }).then((response) => {
const data = response.data
dispatch('users/forceMe', {}, { root: true }).then((data) => {})
return data
})
},
}

View File

@@ -0,0 +1,150 @@
import axios from 'axios'
import { filter } from 'lodash'
import parseISO from 'date-fns/parseISO'
export const state = {
vehicles: [],
roleMasters: [],
fuelUnitMasters: [],
distanceUnitMasters: [],
currencyMasters: [],
fuelTypeMasters: [],
quickEntries: [],
vehicleStats: new Map(),
}
export const getters = {
unprocessedQuickEntries: (state) => {
return filter(state.quickEntries, (o) => o.processDate == null)
},
processedQuickEntries: (state) => {
return filter(state.quickEntries, (o) => o.processDate != null)
},
}
export const mutations = {
CACHE_VEHICLE(state, newVehicles) {
state.vehicles = newVehicles
},
CACHE_VEHICLE_STATS(state, stats) {
state.vehicleStats.set(stats.vehicleId, stats)
},
CACHE_FUEL_UNIT_MASTERS(state, masters) {
state.fuelUnitMasters = masters
},
CACHE_DISTANCE_UNIT_MASTERS(state, masters) {
state.distanceUnitMasters = masters
},
CACHE_FUEL_TYPE_MASTERS(state, masters) {
state.fuelTypeMasters = masters
},
CACHE_CURRENCY_MASTERS(state, masters) {
state.currencyMasters = masters
},
CACHE_QUICK_ENTRIES(state, entries) {
state.quickEntries = entries
},
CACHE_ROLE_MASTERS(state, roles) {
state.roleMasters = roles
},
}
export const actions = {
init({ dispatch, rootState }) {
const { currentUser } = rootState.auth
if (currentUser) {
dispatch('fetchVehicles')
dispatch('fetchMasters')
dispatch('fetchQuickEntries', { force: true })
}
},
fetchMasters({ commit, state, rootState }) {
return axios.get('/api/masters').then((response) => {
const fuelUnitMasters = response.data.fuelUnits
const fuelTypeMasters = response.data.fuelTypes
commit('CACHE_FUEL_UNIT_MASTERS', fuelUnitMasters)
commit('CACHE_FUEL_TYPE_MASTERS', fuelTypeMasters)
commit('CACHE_CURRENCY_MASTERS', response.data.currencies)
commit('CACHE_DISTANCE_UNIT_MASTERS', response.data.distanceUnits)
commit('CACHE_ROLE_MASTERS', response.data.roles)
return response.data
})
},
fetchVehicles({ commit, state, rootState }) {
return axios.get('/api/me/vehicles').then((response) => {
const data = response.data
commit('CACHE_VEHICLE', data)
return data
})
},
fetchQuickEntries({ commit, state, rootState }, { force }) {
if (state.quickEntries && !force) {
return Promise.resolve(state.quickEntries)
}
return axios.get('/api/me/quickEntries').then((response) => {
const data = response.data
commit('CACHE_QUICK_ENTRIES', data)
return data
})
},
fetchVehicleById({ commit, state, rootState }, { vehicleId }) {
const matchedVehicle = state.vehicles.find((vehicle) => vehicle.id === vehicleId)
if (matchedVehicle) {
return Promise.resolve(matchedVehicle)
}
return axios.get('/api/vehicles/' + vehicleId).then((response) => {
const data = response.data
// commit('CACHE_VEHICLE', data)
return data
})
},
fetchFillupById({ commit, state, rootState }, { vehicleId, fillupId }) {
return axios.get(`/api/vehicles/${vehicleId}/fillups/${fillupId}`).then((response) => {
const data = response.data
data.date = parseISO(data.date)
return data
})
},
fetchExpenseById({ commit, state, rootState }, { vehicleId, expenseId }) {
return axios.get(`/api/vehicles/${vehicleId}/expenses/${expenseId}`).then((response) => {
const data = response.data
data.date = parseISO(data.date)
return data
})
},
fetchAttachmentsByVehicleId({ commit, state, rootState }, { vehicleId }) {
return axios.get(`/api/vehicles/${vehicleId}/attachments`).then((response) => {
const data = response.data
return data
})
},
fetchUsersByVehicleId({ commit, state, rootState }, { vehicleId, force }) {
return axios.get(`/api/vehicles/${vehicleId}/users`).then((response) => {
const data = response.data
// data.vehicleId = vehicleId
// commit('CACHE_VEHICLE_STATS', data)
return data
})
},
fetchStatsByVehicleId({ commit, state, rootState }, { vehicleId, force }) {
if (state.vehicleStats.has(vehicleId) && !force) {
return Promise.resolve(state.vehicleStats.get(vehicleId))
}
return axios.get(`/api/vehicles/${vehicleId}/stats`).then((response) => {
const data = response.data
data.vehicleId = vehicleId
commit('CACHE_VEHICLE_STATS', data)
return data
})
},
setQuickEntryAsProcessed({ commit, state, rootState, dispatch }, { id }) {
return axios.post(`/api/quickEntries/${id}/process`, {}).then((response) => {
const data = response.data
dispatch('fetchQuickEntries', { force: true })
return data
})
},
}

21
ui/src/state/store.js Normal file
View File

@@ -0,0 +1,21 @@
import Vue from 'vue'
import Vuex from 'vuex'
import dispatchActionForAllModules from '@utils/dispatch-action-for-all-modules'
import modules from './modules'
Vue.use(Vuex)
const store = new Vuex.Store({
modules,
// Enable strict mode in development to get a warning
// when mutating state outside of a mutation.
// https://vuex.vuejs.org/guide/strict.html
strict: process.env.NODE_ENV !== 'production',
})
export default store
// Automatically run the `init` action for every module,
// if one exists.
dispatchActionForAllModules('init')