@@ -0,0 +1,38 @@ | |||
# Logs | |||
logs | |||
*.log | |||
npm-debug.log* | |||
# Runtime data | |||
pids | |||
*.pid | |||
*.seed | |||
# Directory for instrumented libs generated by jscoverage/JSCover | |||
lib-cov | |||
# Coverage directory used by tools like istanbul | |||
coverage | |||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | |||
.grunt | |||
# node-waf configuration | |||
.lock-wscript | |||
# Compiled binary addons (http://nodejs.org/api/addons.html) | |||
build/Release | |||
# Dependency directories | |||
node_modules | |||
jspm_packages | |||
typings | |||
# Optional npm cache directory | |||
.npm | |||
# Optional REPL history | |||
.node_repl_history | |||
.idea | |||
dist | |||
*~ |
@@ -0,0 +1,25 @@ | |||
# bubble-web | |||
Bubble frontend VueJS web application | |||
The initial basis for the bubble-web frontend was Jason Watmore's wonderfully well-written | |||
[Vue/Vuex Registration and Login Example](https://jasonwatmore.com/post/2018/07/14/vue-vuex-user-registration-and-login-tutorial-example). | |||
The [LICENSE](vue-vuex-registration-login-example-docs/LICENSE) and [README.md](vue-vuex-registration-login-example-docs/README.md) | |||
files for this code are in the [vue-vuex-registration-login-example-docs](vue-vuex-registration-login-example-docs) directory. | |||
To use this frontend with a Bubble API: | |||
Build it: | |||
npm install | |||
webpack | |||
In your `${HOME}/.bubble-test.env` file, add this line (fix the path to point to the correct location of your `bubble-web/dist` directory): | |||
export ASSETS_DIR=/path/to/bubble-web/dist | |||
In the `bubble/bubble-server` directory, run the test named `bubble.test.dev.DevServerTest`, like so: | |||
mvn -Dtest=bubble.test.dev.DevServerTest test | |||
You can now test the frontend. If you make changes to the frontend code, you can simply run `webpack` again and reload the page in your browser. |
@@ -0,0 +1,36 @@ | |||
{ | |||
"name": "vue-vuex-registration-login-example", | |||
"version": "1.0.0", | |||
"repository": { | |||
"type": "git", | |||
"url": "https://github.com/cornflourblue/vue-vuex-registration-login-example.git" | |||
}, | |||
"license": "MIT", | |||
"scripts": { | |||
"start": "webpack-dev-server --open" | |||
}, | |||
"dependencies": { | |||
"safe-eval": "^0.4.1", | |||
"vee-validate": "^2.2.8", | |||
"vue": "^2.6.10", | |||
"vue-router": "^3.0.6", | |||
"vue-select": "^3.4.0", | |||
"vue-sidebar-menu": "^4.4.3", | |||
"vuex": "^3.1.1" | |||
}, | |||
"devDependencies": { | |||
"babel-core": "^6.26.3", | |||
"babel-loader": "^7.1.5", | |||
"babel-preset-env": "^1.7.0", | |||
"babel-preset-stage-3": "^6.24.1", | |||
"babel-preset-vue": "^2.0.2", | |||
"css-loader": "^2.1.1", | |||
"html-webpack-plugin": "^3.2.0", | |||
"path": "^0.12.7", | |||
"vue-loader": "^14.2.3", | |||
"vue-template-compiler": "^2.6.10", | |||
"webpack": "^4.39.3", | |||
"webpack-cli": "^3.3.8", | |||
"webpack-dev-server": "^3.8.0" | |||
} | |||
} |
@@ -0,0 +1,95 @@ | |||
export function currentUser() { | |||
let userJson = localStorage.getItem('user'); | |||
return userJson ? JSON.parse(userJson) : null; | |||
} | |||
export function userLoggedIn() { return !!currentUser(); } | |||
export function authHeader() { | |||
// return authorization header with jwt token | |||
let user = currentUser(); | |||
if (user && user.token) { | |||
return { 'X-Bubble-Session': user.token }; | |||
} else { | |||
return {}; | |||
} | |||
} | |||
export function getWithAuth() { | |||
return { | |||
method: 'GET', | |||
headers: authHeader() | |||
}; | |||
} | |||
export function postWithAuth(obj) { | |||
return { | |||
method: 'POST', | |||
headers: { ...authHeader(), 'Content-Type': 'application/json' }, | |||
body: JSON.stringify(obj) | |||
}; | |||
} | |||
export function putWithAuth(obj) { | |||
return { | |||
method: 'PUT', | |||
headers: { ...authHeader(), 'Content-Type': 'application/json' }, | |||
body: JSON.stringify(obj) | |||
}; | |||
} | |||
export function deleteWithAuth() { | |||
return { | |||
method: 'DELETE', | |||
headers: authHeader() | |||
}; | |||
} | |||
export function handleBasicResponse(response) { | |||
return response.text().then(text => { | |||
const data = text && JSON.parse(text); | |||
if (!response.ok) { | |||
const error = (data && data.message) || response.statusText; | |||
return Promise.reject(error); | |||
} | |||
return data; | |||
}); | |||
} | |||
export function handleCrudResponse(response) { | |||
return response.text().then(text => { | |||
const data = text && JSON.parse(text); | |||
if (!response.ok) { | |||
if (response.status === 404) { | |||
// set error from API | |||
// todo: show nicer error message | |||
console.log('handleResponse: received 404, user not found: '+JSON.stringify(data)); | |||
} else if (response.status === 422) { | |||
// set error from API | |||
// todo: load auth error messages, find text for error message | |||
console.log('handleResponse: received 422, error: '+JSON.stringify(data)); | |||
} | |||
const error = (data && data.message) || response.statusText; | |||
return Promise.reject(error); | |||
} | |||
return data; | |||
}); | |||
} | |||
export function setValidationErrors(data, messages, errors) { | |||
for (let i=0; i<data.length; i++) { | |||
if (data[i].messageTemplate) { | |||
const parts = data[i].messageTemplate.split(/[._]+/); | |||
if (parts.length === 3 && parts[0] === 'err') { | |||
const field = parts[1]; | |||
const messageTemplate = data[i].messageTemplate.replace(/\./g, '_'); | |||
const message = messages.hasOwnProperty(messageTemplate) ? messages[messageTemplate] : '???'+messageTemplate; | |||
errors.add({field: field, msg: message}); | |||
console.log('>>>>> field '+field+' added error: '+message+', errors='+JSON.stringify(errors)); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,2 @@ | |||
export * from './router'; | |||
export * from './api-util'; |
@@ -0,0 +1,53 @@ | |||
import Vue from 'vue'; | |||
import Router from 'vue-router'; | |||
import HomePage from '../account/HomePage' | |||
import RegisterPage from '../auth/RegisterPage' | |||
import LoginPage from '../auth/LoginPage' | |||
import ProfilePage from '../account/ProfilePage' | |||
import NetworksPage from '../account/NetworksPage' | |||
import NewNetworkPage from '../account/NewNetworkPage' | |||
import NetworkPage from '../account/NetworkPage' | |||
import AccountsPage from '../admin/AccountsPage' | |||
import { currentUser } from '../_helpers' | |||
Vue.use(Router); | |||
export const router = new Router({ | |||
mode: 'history', | |||
routes: [ | |||
{ | |||
path: '/', component: HomePage, | |||
children: [ | |||
{ path: '', component: NetworksPage }, | |||
{ path: '/profile', component: ProfilePage }, | |||
{ path: '/networks', component: NetworksPage }, | |||
{ path: '/networks/new', component: NewNetworkPage }, | |||
{ path: '/networks/:uuid', component: NetworkPage } | |||
] | |||
}, | |||
{ path: '/register', component: RegisterPage }, | |||
{ path: '/login', component: LoginPage }, | |||
{ path: '/logout', component: LoginPage }, | |||
{ path: '/admin/accounts', component: AccountsPage }, | |||
{ path: '/admin/accounts/:uuid', component: ProfilePage }, | |||
// otherwise redirect to home | |||
{ path: '*', redirect: '/' } | |||
] | |||
}); | |||
router.beforeEach((to, from, next) => { | |||
const publicPages = ['/login', '/logout', '/register']; | |||
const authRequired = !publicPages.includes(to.path); | |||
const user = currentUser(); | |||
if (authRequired) { | |||
// redirect to login page if not logged in and trying to access a restricted page | |||
if (!user) return next('/login'); | |||
// redirect to home page if not admin and trying to access an admin page | |||
if (to.path.startsWith('/admin') && user.admin !== true) return next('/'); | |||
} | |||
next(); | |||
}); |
@@ -0,0 +1,20 @@ | |||
import config from 'config'; | |||
import { getWithAuth, putWithAuth, handleCrudResponse } from '../_helpers'; | |||
export const accountPlanService = { | |||
getAll, | |||
getById, | |||
newAccountPlan | |||
}; | |||
function getAll(userId) { | |||
return fetch(`${config.apiUrl}/users/${userId}/accountPlans`, getWithAuth()).then(handleCrudResponse); | |||
} | |||
function getById(userId, accountPlanId) { | |||
return fetch(`${config.apiUrl}/users/${userId}/accountPlans/${accountPlanId}`, getWithAuth()).then(handleCrudResponse); | |||
} | |||
function newAccountPlan(userId, accountPlan) { | |||
return fetch(`${config.apiUrl}/users/${userId}/accountPlans`, putWithAuth(accountPlan)).then(handleCrudResponse); | |||
} |
@@ -0,0 +1,15 @@ | |||
import config from 'config'; | |||
import { getWithAuth, handleCrudResponse } from '../_helpers'; | |||
export const domainService = { | |||
getAll, | |||
getById | |||
}; | |||
function getAll(userId) { | |||
return fetch(`${config.apiUrl}/users/${userId}/domains`, getWithAuth()).then(handleCrudResponse); | |||
} | |||
function getById(userId, domainId) { | |||
return fetch(`${config.apiUrl}/users/${userId}/domains/${domainId}`, getWithAuth()).then(handleCrudResponse); | |||
} |
@@ -0,0 +1,15 @@ | |||
import config from 'config'; | |||
import { getWithAuth, handleCrudResponse } from '../_helpers'; | |||
export const footprintService = { | |||
getAll, | |||
getById | |||
}; | |||
function getAll() { | |||
return fetch(`${config.apiUrl}/me/footprints`, getWithAuth()).then(handleCrudResponse); | |||
} | |||
function getById(footprintId) { | |||
return fetch(`${config.apiUrl}/me/footprints/${footprintId}`, getWithAuth()).then(handleCrudResponse); | |||
} |
@@ -0,0 +1,7 @@ | |||
export * from './system.service'; | |||
export * from './user.service'; | |||
export * from './domain.service'; | |||
export * from './plan.service'; | |||
export * from './footprint.service'; | |||
export * from './accountPlan.service'; | |||
export * from './network.service'; |
@@ -0,0 +1,15 @@ | |||
import config from 'config'; | |||
import { getWithAuth, handleCrudResponse } from '../_helpers'; | |||
export const networkService = { | |||
getAll, | |||
getById | |||
}; | |||
function getAll(userId) { | |||
return fetch(`${config.apiUrl}/users/${userId}/networks`, getWithAuth()).then(handleCrudResponse); | |||
} | |||
function getById(userId, networkId) { | |||
return fetch(`${config.apiUrl}/users/${userId}/networks/${networkId}`, getWithAuth()).then(handleCrudResponse); | |||
} |
@@ -0,0 +1,15 @@ | |||
import config from 'config'; | |||
import { getWithAuth, handleCrudResponse } from '../_helpers'; | |||
export const planService = { | |||
getAll, | |||
getById | |||
}; | |||
function getAll() { | |||
return fetch(`${config.apiUrl}/plans`, getWithAuth()).then(handleCrudResponse); | |||
} | |||
function getById(planId) { | |||
return fetch(`${config.apiUrl}/plans/${planId}`, getWithAuth()).then(handleCrudResponse); | |||
} |
@@ -0,0 +1,37 @@ | |||
import config from 'config'; | |||
import { getWithAuth, userLoggedIn, handleBasicResponse } from '../_helpers'; | |||
export const systemService = { | |||
loadSystemConfigs, | |||
loadMessages, | |||
loadTimezones, | |||
detectTimezone | |||
}; | |||
function loadSystemConfigs() { | |||
const requestOptions = userLoggedIn() ? getWithAuth() : { method: 'GET' }; | |||
return fetch(`${config.apiUrl}/auth/configs`, requestOptions) | |||
.then(handleBasicResponse) | |||
.then(configs => { return configs; }); | |||
} | |||
function loadMessages(group, locale) { | |||
const requestOptions = userLoggedIn() ? getWithAuth() : { method: 'GET' }; | |||
if (!locale || locale === '') locale = 'detect'; | |||
return fetch(`${config.apiUrl}/messages/${locale}/${group}`, requestOptions) | |||
.then(handleBasicResponse) | |||
.then(messages => { return messages; }); | |||
} | |||
function loadTimezones() { | |||
const requestOptions = userLoggedIn() ? getWithAuth() : { method: 'GET' }; | |||
return fetch(`${config.apiUrl}/timezones`, requestOptions) | |||
.then(handleBasicResponse) | |||
.then(timezones => { return timezones; }); | |||
} | |||
function detectTimezone() { | |||
return fetch(`${config.apiUrl}/me/detect/timezone`, getWithAuth()) | |||
.then(handleBasicResponse) | |||
.then(timezone => { return timezone; }); | |||
} |
@@ -0,0 +1,95 @@ | |||
import config from 'config'; | |||
import { getWithAuth, postWithAuth, deleteWithAuth, handleCrudResponse, setValidationErrors } from '../_helpers'; | |||
export const userService = { | |||
login, | |||
logout, | |||
register, | |||
getAll, | |||
getById, | |||
update, | |||
delete: _delete | |||
}; | |||
function login(name, password, messages, errors) { | |||
const requestOptions = { | |||
method: 'POST', | |||
headers: { 'Content-Type': 'application/json' }, | |||
body: JSON.stringify({ 'name': name, 'password': password }) | |||
}; | |||
return fetch(`${config.apiUrl}/auth/login`, requestOptions) | |||
.then(handleAuthResponse(messages, errors)) | |||
.then(user => { | |||
// login successful if there's a session token in the response | |||
if (user.token) { | |||
// store user details and session token in local storage to keep user logged in between page refreshes | |||
localStorage.setItem('user', JSON.stringify(user)); | |||
} | |||
return user; | |||
}); | |||
} | |||
function logout() { | |||
// remove user from local storage to log user out | |||
localStorage.removeItem('user'); | |||
} | |||
function register(user, messages, errors) { | |||
const requestOptions = { | |||
method: 'POST', | |||
headers: { 'Content-Type': 'application/json' }, | |||
body: JSON.stringify(user) | |||
}; | |||
return fetch(`${config.apiUrl}/auth/register`, requestOptions) | |||
.then(handleAuthResponse(messages, errors)) | |||
.then(user => { | |||
if (user.token) { | |||
// store user details and session token in local storage to keep user logged in between page refreshes | |||
localStorage.setItem('user', JSON.stringify(user)); | |||
} | |||
return user; | |||
}); | |||
} | |||
function getAll() { | |||
return fetch(`${config.apiUrl}/users`, getWithAuth()).then(handleCrudResponse); | |||
} | |||
function getById(id) { | |||
return fetch(`${config.apiUrl}/users/${id}`, getWithAuth()).then(handleCrudResponse); | |||
} | |||
function update(user) { | |||
return fetch(`${config.apiUrl}/users/${user.uuid}`, postWithAuth(user)).then(handleCrudResponse); | |||
} | |||
// prefixed function name with underscore because delete is a reserved word in javascript | |||
function _delete(id) { | |||
return fetch(`${config.apiUrl}/users/${id}`, deleteWithAuth()).then(handleCrudResponse); | |||
} | |||
function handleAuthResponse(messages, errors) { | |||
return function (response) { | |||
return response.text().then(text => { | |||
const data = text && JSON.parse(text); | |||
if (!response.ok) { | |||
if (response.status === 401) { | |||
// auto logout if 401 response returned from api | |||
logout(); | |||
location.reload(true); | |||
} else if (response.status === 404) { | |||
// set error from API | |||
// todo: show nicer error message | |||
console.log('handleAuthResponse: received 404, user not found: '+JSON.stringify(data)); | |||
} else if (response.status === 422) { | |||
setValidationErrors(data, messages, errors); | |||
} | |||
const error = (data && data.message) || response.statusText; | |||
return Promise.reject(error); | |||
} | |||
return data; | |||
}); | |||
}; | |||
} |
@@ -0,0 +1,104 @@ | |||
import { userService } from '../_services'; | |||
import { router } from '../_helpers'; | |||
// todo: why can't we import currentUser from api-util and use that here? | |||
// when I try to do that, webpack succeeds but then an error occurs loading any page, with the | |||
// error message "_helpers.currentUser is not defined" | |||
const user = JSON.parse(localStorage.getItem('user')); | |||
const state = user | |||
? { status: { loggedIn: true }, user } | |||
: { status: {}, user: null }; | |||
const actions = { | |||
login({ dispatch, commit }, { user, messages, errors }) { | |||
commit('loginRequest', { name: user.name }); | |||
userService.login(user.name, user.password, messages, errors) | |||
.then( | |||
user => { | |||
commit('loginSuccess', user); | |||
router.push('/'); | |||
}, | |||
error => { | |||
commit('loginFailure', error); | |||
dispatch('alert/error', error, { root: true }); | |||
} | |||
); | |||
}, | |||
logout({ commit }) { | |||
userService.logout(); | |||
commit('logout'); | |||
}, | |||
register({ dispatch, commit }, {user, messages, errors}) { | |||
commit('registerRequest', user); | |||
userService.register(user, messages, errors) | |||
.then( | |||
user => { | |||
commit('registerSuccess', user); | |||
router.push('/'); | |||
setTimeout(() => { | |||
// display success message after route change completes | |||
dispatch('alert/success', 'Registration successful', { root: true }); | |||
}) | |||
}, | |||
error => { | |||
commit('registerFailure'); | |||
dispatch('alert/error', error, { root: true }); | |||
} | |||
); | |||
}, | |||
update({ dispatch, commit }, user) { | |||
commit('updateRequest', user); | |||
userService.update(user) | |||
.then( | |||
user => { | |||
commit('updateSuccess', user); | |||
router.push('/me'); | |||
setTimeout(() => { | |||
// display success message after route change completes | |||
dispatch('alert/success', 'Profile update was successful', { root: true }); | |||
}) | |||
}, | |||
error => { | |||
commit('updateFailure', error); | |||
dispatch('alert/error', error, { root: true }); | |||
} | |||
); | |||
} | |||
}; | |||
const mutations = { | |||
loginRequest(state, user) { | |||
state.status = { loggingIn: true }; | |||
state.user = user; | |||
}, | |||
loginSuccess(state, user) { | |||
state.status = { loggedIn: true }; | |||
state.user = user; | |||
}, | |||
loginFailure(state) { | |||
state.status = {}; | |||
state.user = null; | |||
}, | |||
logout(state) { | |||
state.status = {}; | |||
state.user = null; | |||
}, | |||
registerRequest(state, user) { | |||
state.status = { registering: true }; | |||
state.user = user; | |||
}, | |||
registerSuccess(state, user) { | |||
state.status = {}; | |||
state.user = user; | |||
}, | |||
registerFailure(state) { | |||
state.status = {}; | |||
} | |||
}; | |||
export const account = { | |||
namespaced: true, | |||
state, | |||
actions, | |||
mutations | |||
}; |
@@ -0,0 +1,88 @@ | |||
import { accountPlanService } from '../_services'; | |||
import { account } from '../_store/account.module'; | |||
const state = { | |||
loading: null, | |||
error: null, | |||
accountPlans: null, | |||
accountPlan: null | |||
}; | |||
const actions = { | |||
getAll({ commit }) { | |||
commit('getAllRequest'); | |||
accountPlanService.getAll(account.user.uuid) | |||
.then( | |||
accountPlans => commit('getAllSuccess', accountPlans), | |||
error => commit('getAllFailure', error) | |||
); | |||
}, | |||
getByUuid({ commit }, uuid) { | |||
commit('getByUuidRequest'); | |||
accountPlanService.getById(account.user.uuid, uuid) | |||
.then( | |||
accountPlan => commit('getByUuidSuccess', accountPlan), | |||
error => commit('getByUuidFailure', error) | |||
); | |||
}, | |||
delete({ commit }, id) { | |||
commit('deleteRequest', id); | |||
accountPlanService.delete(account.user.uuid, id) | |||
.then( | |||
accountPlan => commit('deleteSuccess', accountPlan), | |||
error => commit('deleteFailure', { id, error: error.toString() }) | |||
); | |||
} | |||
}; | |||
const mutations = { | |||
getAllRequest(state) { | |||
state.loading = true; | |||
}, | |||
getAllSuccess(state, accountPlans) { | |||
state.loading = false; | |||
state.accountPlans = accountPlans; | |||
}, | |||
getAllFailure(state, error) { | |||
state.loading = false; | |||
state.error = { error }; | |||
}, | |||
getByUuidRequest(state) { | |||
state.loading = true; | |||
}, | |||
getByUuidSuccess(state, accountPlan) { | |||
state.loading = false; | |||
state.accountPlan = accountPlan; | |||
}, | |||
getByUuidFailure(state, error) { | |||
state.loading = false; | |||
state.error = { error }; | |||
}, | |||
deleteRequest(state, id) { | |||
state.loading = true; | |||
}, | |||
deleteSuccess(state, id) { | |||
state.loading = false; | |||
// remove deleted accountPlan from state | |||
if (state.accountPlans) { | |||
state.accountPlans = state.accountPlans.filter(accountPlan => accountPlan.uuid !== id) | |||
} | |||
}, | |||
deleteFailure(state, { id, error }) { | |||
state.loading = false; | |||
// remove 'deleting:true' property and add 'deleteError:[error]' property to accountPlan | |||
state.error = error; | |||
} | |||
}; | |||
export const accountPlans = { | |||
namespaced: true, | |||
state, | |||
actions, | |||
mutations | |||
}; |
@@ -0,0 +1,38 @@ | |||
const state = { | |||
type: null, | |||
message: null | |||
}; | |||
const actions = { | |||
success({ commit }, message) { | |||
commit('success', message); | |||
}, | |||
error({ commit }, message) { | |||
commit('error', message); | |||
}, | |||
clear({ commit }, message) { | |||
commit('success', message); | |||
} | |||
}; | |||
const mutations = { | |||
success(state, message) { | |||
state.type = 'alert-success'; | |||
state.message = message; | |||
}, | |||
error(state, message) { | |||
state.type = 'alert-danger'; | |||
state.message = message; | |||
}, | |||
clear(state) { | |||
state.type = null; | |||
state.message = null; | |||
} | |||
}; | |||
export const alert = { | |||
namespaced: true, | |||
state, | |||
actions, | |||
mutations | |||
}; |
@@ -0,0 +1,60 @@ | |||
import { domainService } from '../_services'; | |||
const state = { | |||
loading: null, | |||
error: null, | |||
domains: null, | |||
domain: null | |||
}; | |||
const actions = { | |||
getAll({ commit }, userId) { | |||
commit('getAllRequest'); | |||
domainService.getAll(userId) | |||
.then( | |||
domains => commit('getAllSuccess', domains), | |||
error => commit('getAllFailure', error) | |||
); | |||
}, | |||
getByUuid({ commit }, userId, uuid) { | |||
commit('getByUuidRequest'); | |||
domainService.getById(userId, uuid) | |||
.then( | |||
domain => commit('getByUuidSuccess', domain), | |||
error => commit('getByUuidFailure', error) | |||
); | |||
} | |||
}; | |||
const mutations = { | |||
getAllRequest(state) { | |||
state.loading = true; | |||
}, | |||
getAllSuccess(state, domains) { | |||
state.loading = false; | |||
state.domains = domains; | |||
}, | |||
getAllFailure(state, error) { | |||
state.loading = false; | |||
state.error = { error }; | |||
}, | |||
getByUuidRequest(state) { | |||
state.loading = true; | |||
}, | |||
getByUuidSuccess(state, domain) { | |||
state.loading = false; | |||
state.domain = domain; | |||
}, | |||
getByUuidFailure(state, error) { | |||
state.loading = false; | |||
state.error = { error }; | |||
} | |||
}; | |||
export const domains = { | |||
namespaced: true, | |||
state, | |||
actions, | |||
mutations | |||
}; |
@@ -0,0 +1,60 @@ | |||
import { footprintService } from '../_services'; | |||
const state = { | |||
loading: null, | |||
error: null, | |||
footprints: null, | |||
footprint: null | |||
}; | |||
const actions = { | |||
getAll({ commit }) { | |||
commit('getAllRequest'); | |||
footprintService.getAll() | |||
.then( | |||
footprints => commit('getAllSuccess', footprints), | |||
error => commit('getAllFailure', error) | |||
); | |||
}, | |||
getByUuid({ commit }, uuid) { | |||
commit('getByUuidRequest'); | |||
footprintService.getById(uuid) | |||
.then( | |||
footprint => commit('getByUuidSuccess', footprint), | |||
error => commit('getByUuidFailure', error) | |||
); | |||
} | |||
}; | |||
const mutations = { | |||
getAllRequest(state) { | |||
state.loading = true; | |||
}, | |||
getAllSuccess(state, footprints) { | |||
state.loading = false; | |||
state.footprints = footprints; | |||
}, | |||
getAllFailure(state, error) { | |||
state.loading = false; | |||
state.error = { error }; | |||
}, | |||
getByUuidRequest(state) { | |||
state.loading = true; | |||
}, | |||
getByUuidSuccess(state, footprint) { | |||
state.loading = false; | |||
state.footprint = footprint; | |||
}, | |||
getByUuidFailure(state, error) { | |||
state.loading = false; | |||
state.error = { error }; | |||
} | |||
}; | |||
export const footprints = { | |||
namespaced: true, | |||
state, | |||
actions, | |||
mutations | |||
}; |
@@ -0,0 +1,50 @@ | |||
import Vue from 'vue'; | |||
import Vuex from 'vuex'; | |||
import { alert } from './alert.module'; | |||
import { system } from './system.module'; | |||
import { account } from './account.module'; | |||
import { users } from './users.module'; | |||
import { plans } from './plans.module'; | |||
import { footprints } from './footprints.module'; | |||
import { domains } from './domains.module'; | |||
import { accountPlans } from './accountPlans.module'; | |||
import { networks } from './networks.module'; | |||
Vue.use(Vuex); | |||
export const store = new Vuex.Store({ | |||
modules: { | |||
alert, | |||
system, | |||
account, | |||
users, | |||
domains, | |||
plans, | |||
footprints, | |||
accountPlans, | |||
networks | |||
} | |||
}); | |||
const safeEval = require('safe-eval'); | |||
function evalInContext(vue, string) { | |||
try { | |||
return safeEval('vue.'+string, {vue}); | |||
} catch (error) { | |||
try { | |||
return safeEval(string); | |||
} catch (errorWithoutThis) { | |||
console.warn('evalInContext: Error evaluating "' + string + '"', errorWithoutThis); | |||
return ''; | |||
} | |||
} | |||
} | |||
String.prototype.parseMessage = function (vue) { | |||
return this ? ''+this.replace(/{{.*?}}/g, match => { | |||
const expression = match.slice(2, -2); | |||
return evalInContext(vue, expression) | |||
}) : ''; | |||
}; |
@@ -0,0 +1,90 @@ | |||
import { networkService } from '../_services'; | |||
import { account } from '../_store/account.module'; | |||
const state = { | |||
loading: null, | |||
creating: null, | |||
error: null, | |||
networks: null, | |||
network: null | |||
}; | |||
const actions = { | |||
getAll({ commit }) { | |||
commit('getAllRequest'); | |||
networkService.getAll(account.state.user.uuid) | |||
.then( | |||
networks => commit('getAllSuccess', networks), | |||
error => commit('getAllFailure', error) | |||
); | |||
}, | |||
getByUuid({ commit }, uuid) { | |||
commit('getByUuidRequest'); | |||
networkService.getById(account.state.user.uuid, uuid) | |||
.then( | |||
network => commit('getByUuidSuccess', network), | |||
error => commit('getByUuidFailure', error) | |||
); | |||
}, | |||
create({ commit }, accountPlan) { | |||
// todo | |||
}, | |||
delete({ commit }, id) { | |||
commit('deleteRequest', id); | |||
networkService.delete(account.state.user.uuid, id) | |||
.then( | |||
network => commit('deleteSuccess', network), | |||
error => commit('deleteFailure', { id, error: error.toString() }) | |||
); | |||
} | |||
}; | |||
const mutations = { | |||
getAllRequest(state) { | |||
state.loading = true; | |||
}, | |||
getAllSuccess(state, networks) { | |||
state.loading = false; | |||
state.networks = networks; | |||
}, | |||
getAllFailure(state, error) { | |||
state.loading = false; | |||
state.error = { error }; | |||
}, | |||
getByUuidRequest(state) { | |||
state.loading = true; | |||
}, | |||
getByUuidSuccess(state, network) { | |||
state.loading = false; | |||
state.network = network; | |||
}, | |||
getByUuidFailure(state, error) { | |||
state.loading = false; | |||
state.error = { error }; | |||
}, | |||
deleteRequest(state, id) { | |||
state.loading = true; | |||
}, | |||
deleteSuccess(state, id) { | |||
state.loading = false; | |||
// remove deleted network from state | |||
if (state.networks) { | |||
state.networks = state.networks.filter(network => network.uuid !== id) | |||
} | |||
}, | |||
deleteFailure(state, { id, error }) { | |||
state.loading = false; | |||
// remove 'deleting:true' property and add 'deleteError:[error]' property to network | |||
state.error = error; | |||
} | |||
}; | |||
export const networks = { | |||
namespaced: true, | |||
state, | |||
actions, | |||
mutations | |||
}; |
@@ -0,0 +1,84 @@ | |||
import { planService } from '../_services'; | |||
const state = { | |||
loading: null, | |||
error: null, | |||
plans: null, | |||
plan: null | |||
}; | |||
const actions = { | |||
getAll({ commit }) { | |||
commit('getAllRequest'); | |||
planService.getAll() | |||
.then( | |||
plans => commit('getAllSuccess', plans), | |||
error => commit('getAllFailure', error) | |||
); | |||
}, | |||
getByUuid({ commit }, uuid) { | |||
commit('getByUuidRequest'); | |||
planService.getById(uuid) | |||
.then( | |||
plan => commit('getByUuidSuccess', plan), | |||
error => commit('getByUuidFailure', error) | |||
); | |||
}, | |||
delete({ commit }, id) { | |||
commit('deleteRequest', id); | |||
planService.delete(id) | |||
.then( | |||
plan => commit('deleteSuccess', plan), | |||
error => commit('deleteFailure', { id, error: error.toString() }) | |||
); | |||
} | |||
}; | |||
const mutations = { | |||
getAllRequest(state) { | |||
state.loading = true; | |||
}, | |||
getAllSuccess(state, plans) { | |||
state.loading = false; | |||
state.plans = plans; | |||
}, | |||
getAllFailure(state, error) { | |||
state.loading = false; | |||
state.error = { error }; | |||
}, | |||
getByUuidRequest(state) { | |||
state.loading = true; | |||
}, | |||
getByUuidSuccess(state, plan) { | |||
state.loading = false; | |||
state.plan = plan; | |||
}, | |||
getByUuidFailure(state, error) { | |||
state.loading = false; | |||
state.error = { error }; | |||
}, | |||
deleteRequest(state, id) { | |||
state.loading = true; | |||
}, | |||
deleteSuccess(state, id) { | |||
state.loading = false; | |||
// remove deleted plan from state | |||
if (state.plans) { | |||
state.plans = state.plans.filter(plan => plan.uuid !== id) | |||
} | |||
}, | |||
deleteFailure(state, { id, error }) { | |||
state.loading = false; | |||
// remove 'deleting:true' property and add 'deleteError:[error]' property to user | |||
state.error = error; | |||
} | |||
}; | |||
export const plans = { | |||
namespaced: true, | |||
state, | |||
actions, | |||
mutations | |||
}; |
@@ -0,0 +1,128 @@ | |||
import { systemService } from '../_services'; | |||
const state = { | |||
configs: { | |||
allowRegistration: null | |||
}, | |||
messages: {}, | |||
countries: [], | |||
locales: [], | |||
timezones: [], | |||
detectedTimezone: null, | |||
menu: [], | |||
error: null | |||
}; | |||
const actions = { | |||
loadSystemConfigs({ commit }) { | |||
commit('loadSystemConfigsRequest'); | |||
systemService.loadSystemConfigs() | |||
.then( | |||
configs => commit('loadSystemConfigsSuccess', configs), | |||
error => commit('loadSystemConfigsFailure', error) | |||
); | |||
}, | |||
loadMessages({ commit }, group, locale) { | |||
commit('loadMessagesRequest'); | |||
systemService.loadMessages(group, locale) | |||
.then( | |||
messages => commit('loadMessagesSuccess', {group, messages}), | |||
error => commit('loadMessagesFailure', error) | |||
); | |||
}, | |||
loadTimezones({ commit }) { | |||
commit('loadTimezonesRequest'); | |||
systemService.loadTimezones().then( | |||
timezones => commit('loadTimezonesSuccess', timezones), | |||
error => commit('loadTimezonesFailure', error) | |||
) | |||
}, | |||
detectTimezone({ commit }) { | |||
commit('detectTimezoneRequest'); | |||
systemService.detectTimezone().then( | |||
timezones => commit('detectTimezoneSuccess', timezones), | |||
error => commit('detectTimezoneFailure', error) | |||
) | |||
} | |||
}; | |||
const mutations = { | |||
loadSystemConfigsRequest(state) {}, | |||
loadSystemConfigsSuccess(state, configs) { | |||
state.configs = configs; | |||
}, | |||
loadSystemConfigsFailure(state, error) { | |||
state.error = error; | |||
}, | |||
loadMessagesRequest(state) {}, | |||
loadMessagesSuccess(state, {group, messages}) { | |||
// console.log('loadMessages (group='+group+'), messages='+JSON.stringify(messages)); | |||
state.messages = Object.assign({}, state.messages, messages); | |||
state.menu = [{ | |||
href: '/', | |||
title: messages.label_menu_dashboard, | |||
icon: messages.label_menu_dashboard_icon | |||
}, { | |||
href: '/profile', | |||
title: messages.label_menu_account, | |||
icon: messages.label_menu_account_icon, | |||
child: [{ | |||
href: '/profile/policy', | |||
title: messages.label_menu_account_policy, | |||
icon: messages.label_menu_account_policy_icon | |||
}, { | |||
href: '/profile/contacts', | |||
title: messages.label_menu_account_contacts, | |||
icon: messages.label_menu_account_contacts_icon | |||
}] | |||
}, { | |||
href: '/me/networks', | |||
title: messages.label_menu_networks, | |||
icon: messages.label_menu_networks_icon | |||
}, { | |||
href: '/logout', | |||
title: messages.label_menu_logout, | |||
icon: messages.label_menu_logout_icon | |||
}]; | |||
if (messages.country_codes) { | |||
const countries = []; | |||
const codes = messages.country_codes.split(','); | |||
for (let i=0; i<codes.length; i++) { | |||
countries.push({code: codes[i], countryName: messages['country_'+codes[i]]}); | |||
} | |||
state.countries = countries; | |||
} | |||
if (messages.locale_codes) { | |||
const locales = []; | |||
const codes = messages.locale_codes.split(','); | |||
for (let i=0; i<codes.length; i++) { | |||
locales.push({localeCode: codes[i], localeName: messages['locale_'+codes[i]]}); | |||
} | |||
state.locales = locales; | |||
} | |||
}, | |||
loadMessagesFailure(state, error) { | |||
state.error = error; | |||
}, | |||
loadTimezonesRequest(state) {}, | |||
loadTimezonesSuccess(state, timezones) { | |||
state.timezones = timezones; | |||
}, | |||
loadTimezonesFailure(state, error) { | |||
state.error = error; | |||
}, | |||
detectTimezoneRequest(state) {}, | |||
detectTimezoneSuccess(state, detectedTimezone) { | |||
state.detectedTimezone = detectedTimezone; | |||
}, | |||
detectTimezoneFailure(state, error) { | |||
state.error = error; | |||
} | |||
}; | |||
export const system = { | |||
namespaced: true, | |||
state, | |||
actions, | |||
mutations | |||
}; |
@@ -0,0 +1,114 @@ | |||
import { userService } from '../_services'; | |||
import { account } from '../_store/account.module'; | |||
const state = { | |||
all: {}, | |||
user: null | |||
}; | |||
const actions = { | |||
getAll({ commit }) { | |||
commit('getAllRequest'); | |||
userService.getAll() | |||
.then( | |||
users => commit('getAllSuccess', users), | |||
error => commit('getAllFailure', error) | |||
); | |||
}, | |||
getByUuid({ commit }, uuid) { | |||
commit('getByUuidRequest'); | |||
userService.getById(uuid) | |||
.then( | |||
users => commit('getByUuidSuccess', users), | |||
error => commit('getByUuidFailure', error) | |||
); | |||
}, | |||
update({ commit }, user) { | |||
commit('updateRequest', user); | |||
userService.update(user) | |||
.then( | |||
user => commit('updateSuccess', user), | |||
error => commit('updateFailure', { user, error: error.toString() }) | |||
); | |||
}, | |||
delete({ commit }, id) { | |||
commit('deleteRequest', id); | |||
userService.delete(id) | |||
.then( | |||
id => commit('deleteSuccess', id), | |||
error => commit('deleteFailure', { id, error: error.toString() }) | |||
); | |||
} | |||
}; | |||
const mutations = { | |||
getAllRequest(state) { | |||
state.all = { loading: true }; | |||
}, | |||
getAllSuccess(state, users) { | |||
state.all = { items: users }; | |||
}, | |||
getAllFailure(state, error) { | |||
state.all = { error }; | |||
}, | |||
getByUuidRequest(state) { | |||
state.user = { loading: true }; | |||
}, | |||
getByUuidSuccess(state, user) { | |||
state.user = user; | |||
}, | |||
getByUuidFailure(state, error) { | |||
state.user = { error }; | |||
}, | |||
updateRequest(state, user) { | |||
// todo: add 'updating:true' property to user being updated | |||
}, | |||
updateSuccess(state, user) { | |||
// todo: why doesn't this work? | |||
user.token = account.user.token; // preserve token | |||
state.user = account.user = user; | |||
localStorage.setItem('user', JSON.stringify(user)); | |||
}, | |||
updateFailure(state, { id, error }) { | |||
// todo: remove 'updating:true' property and add 'updateError:[error]' property to user | |||
}, | |||
deleteRequest(state, id) { | |||
// add 'deleting:true' property to user being deleted | |||
state.all.items = state.all.items.map(user => | |||
user.uuid === id | |||
? { ...user, deleting: true } | |||
: user | |||
) | |||
}, | |||
deleteSuccess(state, id) { | |||
// remove deleted user from state | |||
state.all.items = state.all.items.filter(user => user.uuid !== id) | |||
}, | |||
deleteFailure(state, { id, error }) { | |||
// remove 'deleting:true' property and add 'deleteError:[error]' property to user | |||
state.all.items = state.items.map(user => { | |||
if (user.uuid === id) { | |||
// make copy of user without 'deleting:true' property | |||
const { deleting, ...userCopy } = user; | |||
// return copy of user with 'deleteError:[error]' property | |||
return { ...userCopy, deleteError: error }; | |||
} | |||
return user; | |||
}) | |||
} | |||
}; | |||
export const users = { | |||
namespaced: true, | |||
state, | |||
actions, | |||
mutations | |||
}; |
@@ -0,0 +1,33 @@ | |||
<template> | |||
<div> | |||
<h1>{{messages.label_homepage_hello && messages.label_homepage_hello.parseMessage(this)}}!</h1> | |||
<hr/> | |||
<router-view></router-view> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions } from 'vuex' | |||
export default { | |||
computed: { | |||
...mapState({ | |||
account: state => state.account, | |||
users: state => state.users.all | |||
}), | |||
...mapState('system', ['messages']) | |||
}, | |||
created () { | |||
// todo: allow user to choose locale | |||
const locale = 'detect'; | |||
this.loadMessages('post_auth', locale); | |||
}, | |||
methods: { | |||
...mapActions('users', { | |||
getAllUsers: 'getAll', | |||
deleteUser: 'delete' | |||
}), | |||
...mapActions('system', ['loadMessages', 'loadTimezones']) | |||
} | |||
}; | |||
</script> |
@@ -0,0 +1,28 @@ | |||
<template> | |||
<div> | |||
Single network page | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions } from 'vuex' | |||
export default { | |||
computed: { | |||
...mapState({ | |||
loading: state => state.loading, | |||
network: state => state.network, | |||
error: state => state.error | |||
}) | |||
}, | |||
created () { | |||
this.getById(this.$route.params.uuid); | |||
}, | |||
methods: { | |||
...mapActions('networks', { | |||
getById: 'getByUuid', | |||
deleteNetwork: 'delete' | |||
}) | |||
} | |||
}; | |||
</script> |
@@ -0,0 +1,61 @@ | |||
<template> | |||
<div> | |||
<em v-if="loading">{{messages.loading_networks}}</em> | |||
<h2>{{messages.table_title_networks}}</h2> | |||
<div v-if="!networks || networks.length === 0"> | |||
{{messages.empty_networks}} | |||
</div> | |||
<div v-if="networks && networks.length > 0"> | |||
<table border="1"> | |||
<thead> | |||
<tr> | |||
<th nowrap="nowrap">{{messages.table_head_networks_name}}</th> | |||
<th nowrap="nowrap">{{messages.table_head_networks_locale}}</th> | |||
<th nowrap="nowrap">{{messages.table_head_networks_timezone}}</th> | |||
<th nowrap="nowrap">{{messages.table_head_networks_object_state}}</th> | |||
<th nowrap="nowrap">{{messages.table_head_networks_action_view}}</th> | |||
<th nowrap="nowrap">{{messages.table_head_networks_action_start_stop}}</th> | |||
<th nowrap="nowrap">{{messages.table_head_networks_action_delete}}</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr v-for="network in networks" :key="network.uuid"> | |||
<td>{{network.name}}.{{network.domainName}}</td> | |||
<td>{{network.locale}}</td> | |||
<td>{{network.timezone}}</td> | |||
<td>{{network.state}}</td> | |||
<td><router-link :to="{ path: '/networks/'+ network.uuid }">{{messages.table_row_networks_action_view}}</router-link></td> | |||
<td v-if="network.state === 'running'">{{messages.table_row_networks_action_stop}}</td> | |||
<td v-if="network.state === 'created'">{{messages.table_row_networks_action_start}}</td> | |||
<td><a @click="deleteNetwork(network.uuid)" class="text-danger">{{messages.table_row_networks_action_delete}}</a></td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</div> | |||
<hr/> | |||
<router-link to="/networks/new">{{messages.button_label_new_network}}</router-link> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions } from 'vuex' | |||
export default { | |||
computed: { | |||
...mapState({ | |||
loading: state => state.networks.loading, | |||
networks: state => state.networks.networks | |||
}), | |||
...mapState('system', ['messages']) | |||
}, | |||
created () { | |||
this.getAllNetworks(); | |||
}, | |||
methods: { | |||
...mapActions('networks', { | |||
getAllNetworks: 'getAll', | |||
deleteNetwork: 'delete' | |||
}) | |||
} | |||
}; | |||
</script> |
@@ -0,0 +1,140 @@ | |||
<template> | |||
<div> | |||
<h2>{{messages.form_label_title_new_network}}</h2> | |||
<form @submit.prevent="handleSubmit"> | |||
<div class="form-group"> | |||
<label for="name">{{messages.field_label_network_name}}</label> | |||
<input type="text" v-model="network.name" v-validate="'required'" name="name" class="form-control" :class="{ 'is-invalid': submitted && errors.has('name') }" /> | |||
<v-select v-if="domains" :options="domains" label="name" type="text" v-model="network.domain" name="domain" class="form-control" :class="{ 'is-invalid': submitted && errors.has('domain') }"></v-select> | |||
<div v-if="submitted && errors.has('name')" class="invalid-feedback">{{ errors.first('name') }}</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="locale">{{messages.field_label_locale}}</label> | |||
<v-select v-if="locales" :options="locales" :reduce="locale => locale.localeCode" label="localeName" type="text" v-model="network.locale" name="locale" class="form-control" :class="{ 'is-invalid': submitted && errors.has('locale') }"></v-select> | |||
<div v-if="submitted && errors.has('locale')" class="invalid-feedback">{{ errors.first('locale') }}</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="timezone">{{messages.field_label_timezone}}</label> | |||
<v-select :options="timezoneObjects" :reduce="tz => tz.timezoneId" label="timezoneDescription" :value="detectedTimezone ? detectedTimezone.timeZoneId : null" type="text" v-model="network.timezone" name="timezone" class="form-control" :class="{ 'is-invalid': submitted && errors.has('timezone') }"></v-select> | |||
<div v-if="submitted && errors.has('timezone')" class="invalid-feedback">{{ errors.first('timezone') }}</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="plan">{{messages.field_label_plan}}</label> | |||
<v-select v-if="planObjects" :options="planObjects" :reduce="plan => plan.name" label="localName" type="text" v-model="network.plan" name="plan" class="form-control" :class="{ 'is-invalid': submitted && errors.has('plan') }"></v-select> | |||
<div v-if="submitted && errors.has('plan')" class="invalid-feedback">{{ errors.first('plan') }}</div> | |||
<div> | |||
{{messages['plan_description_'+network.plan]}} | |||
</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="footprint">{{messages.field_label_footprint}}</label> | |||
<v-select v-if="footprintObjects" :options="footprintObjects" :reduce="footprint => footprint.name" label="name" :value="network.footprint" type="text" v-model="network.footprint" name="footprint" class="form-control" :class="{ 'is-invalid': submitted && errors.has('footprint') }"></v-select> | |||
<div v-if="submitted && errors.has('footprint')" class="invalid-feedback">{{ errors.first('footprint') }}</div> | |||
<div> | |||
{{messages['footprint_description_'+network.footprint]}} | |||
</div> | |||
</div> | |||
<div class="form-group"> | |||
<button class="btn btn-primary" :disabled="status.creating">{{messages.button_label_create_new_network}}</button> | |||
<img v-show="status.creating" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" /> | |||
<router-link to="/" class="btn btn-link">{{messages.button_label_cancel}}</router-link> | |||
</div> | |||
</form> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions } from 'vuex' | |||
import { currentUser } from '../_helpers' | |||
export default { | |||
data() { | |||
return { | |||
network: { | |||
name: '', | |||
domain: '', | |||
locale: 'en_US', | |||
timezone: '', | |||
plan: 'bubble', | |||
footprint: 'Worldwide' | |||
}, | |||
user: currentUser(), | |||
submitted: false, | |||
status: '' | |||
}; | |||
}, | |||
computed: { | |||
...mapState('system', ['messages', 'locales', 'timezones', 'detectedTimezone']), | |||
...mapState('domains', ['domains']), | |||
...mapState('plans', ['plans']), | |||
...mapState('footprints', ['footprints']), | |||
...mapState('networks', { | |||
creating: state => state.loading, | |||
error: state => state.error | |||
}), | |||
timezoneObjects: function () { | |||
const tz_objects = []; | |||
for (let i=0; i<this.timezones.length; i++) { | |||
tz_objects.push({ | |||
timezoneId: this.timezones[i], | |||
timezoneDescription: this.messages['tz_name_'+this.timezones[i]] + " - " + this.messages['tz_description_'+this.timezones[i]] | |||
}) | |||
} | |||
return tz_objects; | |||
}, | |||
planObjects: function () { | |||
const plans_array = []; | |||
if (this.plans) { | |||
for (let i = 0; i < this.plans.length; i++) { | |||
plans_array.push({ | |||
...this.plans[i], | |||
localName: this.messages['plan_name_' + this.plans[i].name], | |||
description: this.messages['plan_description_' + this.plans[i].name] | |||
}) | |||
} | |||
} | |||
return plans_array; | |||
}, | |||
footprintObjects: function () { | |||
const fp_array = []; | |||
if (this.footprints) { | |||
for (let i = 0; i < this.footprints.length; i++) { | |||
fp_array.push({ | |||
...this.footprints[i], | |||
localName: this.messages['footprint_name_' + this.footprints[i].name], | |||
description: this.messages['footprint_description_' + this.footprints[i].name] | |||
}) | |||
} | |||
} | |||
return fp_array; | |||
} | |||
}, | |||
methods: { | |||
...mapActions('networks', { | |||
createNewNetwork: 'create' | |||
}), | |||
...mapActions('domains', { | |||
loadDomains: 'getAll' | |||
}), | |||
...mapActions('plans', { | |||
loadPlans: 'getAll' | |||
}), | |||
...mapActions('footprints', { | |||
loadFootprints: 'getAll' | |||
}), | |||
handleSubmit(e) { | |||
this.submitted = true; | |||
this.$validator.validate().then(valid => { | |||
if (valid) { | |||
this.createNewNetwork(this.network); | |||
} | |||
}); | |||
} | |||
}, | |||
created() { | |||
this.loadDomains(currentUser().uuid); | |||
this.loadPlans(); | |||
this.loadFootprints(); | |||
} | |||
}; | |||
</script> |
@@ -0,0 +1,93 @@ | |||
<template> | |||
<div> | |||
<h2>Edit Profile</h2> | |||
<form @submit.prevent="handleSubmit"> | |||
<div class="form-group"> | |||
<label for="name">Username</label> | |||
<input type="text" v-model="name" name="name" class="form-control" :class="{ 'is-invalid': submitted && !name }" /> | |||
<div v-show="submitted && !name" class="invalid-feedback">Name is required</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="url">Url</label> | |||
<input type="text" v-model="url" name="url" class="form-control"/> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="description">Description</label> | |||
<input type="text" v-model="description" name="description" class="form-control"/> | |||
</div> | |||
<div class="form-group"> | |||
<label for="admin">Admin</label> | |||
<input type="checkbox" id="admin" v-model="admin"> | |||
</div> | |||
<div class="form-group"> | |||
<label for="suspended">Suspended</label> | |||
<input type="checkbox" id="suspended" v-model="suspended"> | |||
</div> | |||
<div> | |||
<h3>Auto-Update Policy</h3> | |||
<div class="form-group"> | |||
<label for="autoUpdatePolicy.driverUpdates">Drivers</label> | |||
<input type="checkbox" id="autoUpdatePolicy.driverUpdates" v-model="autoUpdatePolicy.driverUpdates"> | |||
</div> | |||
<div class="form-group"> | |||
<label for="autoUpdatePolicy.appUpdates">Apps</label> | |||
<input type="checkbox" id="autoUpdatePolicy.appUpdates" v-model="autoUpdatePolicy.appUpdates"> | |||
</div> | |||
<div class="form-group"> | |||
<label for="autoUpdatePolicy.dataUpdates">App Data</label> | |||
<input type="checkbox" id="autoUpdatePolicy.dataUpdates" v-model="autoUpdatePolicy.dataUpdates"> | |||
</div> | |||
<div class="form-group"> | |||
<label for="autoUpdatePolicy.newStuff">New Drivers/Apps</label> | |||
<input type="checkbox" id="autoUpdatePolicy.newStuff" v-model="autoUpdatePolicy.newStuff"> | |||
</div> | |||
</div> | |||
<div class="form-group"> | |||
<button class="btn btn-primary" :disabled="status.updating">Update</button> | |||
<img v-show="status.updating" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" /> | |||
</div> | |||
</form> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions } from 'vuex' | |||
import { account } from '../_store/account.module' | |||
import { currentUser } from '../_helpers' | |||
export default { | |||
data () { | |||
if (this.$router.params && this.$router.params.uuid) { | |||
return { | |||
...this.loadUser($this.router.params.uuid), | |||
submitted: false | |||
} | |||
} | |||
return { | |||
...currentUser(), | |||
submitted: false | |||
}; | |||
}, | |||
computed: { | |||
...mapState('account', ['status']) | |||
}, | |||
methods: { | |||
...mapActions('users', ['update', 'loadUser']), | |||
handleSubmit (e) { | |||
this.submitted = true; | |||
const updatedProfile = { | |||
uuid: this.uuid, | |||
name: this.name, | |||
url: this.url, | |||
description: this.description, | |||
admin: this.admin, | |||
suspended: this.suspended, | |||
autoUpdatePolicy: this.autoUpdatePolicy | |||
}; | |||
if (this.name) { | |||
this.update(updatedProfile); | |||
} | |||
} | |||
} | |||
}; | |||
</script> |
@@ -0,0 +1,42 @@ | |||
<template> | |||
<div> | |||
<h1>Hi {{account.user.name}}!</h1> | |||
<em v-if="users.loading">Loading users...</em> | |||
<span v-if="users.error" class="text-danger">ERROR: {{users.error}}</span> | |||
<ul v-if="users.items"> | |||
<li v-for="user in users.items" :key="user.id"> | |||
{{user.name}} | |||
<span v-if="user.deleting"><em> - Deleting...</em></span> | |||
<span v-else-if="user.deleteError" class="text-danger"> - ERROR: {{user.deleteError}}</span> | |||
<span v-else> | |||
<router-link to="/me">Edit</router-link> | |||
<span v-if="user.name !== account.user.name"> | |||
- <a @click="deleteUser(user.uuid)" class="text-danger">Delete</a> | |||
</span> | |||
</span> | |||
</li> | |||
</ul> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions } from 'vuex' | |||
export default { | |||
computed: { | |||
...mapState({ | |||
account: state => state.account, | |||
users: state => state.users.all | |||
}) | |||
}, | |||
created () { | |||
this.getAllUsers(); | |||
}, | |||
methods: { | |||
...mapActions('users', { | |||
getAllUsers: 'getAll', | |||
deleteUser: 'delete' | |||
}) | |||
} | |||
}; | |||
</script> |
@@ -0,0 +1,48 @@ | |||
<template> | |||
<div class="jumbotron"> | |||
<sidebar-menu :hide-toggle="true" :menu="menu" v-if="status.loggedIn"/> | |||
<div class="container"> | |||
<div class="row"> | |||
<div class="col-sm-6 offset-sm-3"> | |||
<div v-if="alert.message" :class="`alert ${alert.type}`">{{alert.message}}</div> | |||
<router-view></router-view> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions } from 'vuex' | |||
export default { | |||
name: 'app', | |||
computed: { | |||
...mapState('account', ['status']), | |||
...mapState('system', ['configs', 'messages', 'menu']), | |||
...mapState({ | |||
alert: state => state.alert | |||
}) | |||
}, | |||
methods: { | |||
...mapActions({ clearAlert: 'alert/clear' }), | |||
...mapActions('system', ['loadSystemConfigs', 'loadMessages', 'loadTimezones', 'detectTimezone']) | |||
}, | |||
watch: { | |||
$route (to, from){ | |||
// clear alert on location change | |||
this.clearAlert(); | |||
} | |||
}, | |||
created() { | |||
// todo: allow user to choose locale | |||
const locale = 'detect'; | |||
this.loadTimezones(); | |||
this.loadMessages('pre_auth', locale); | |||
this.loadMessages('countries', locale); | |||
this.loadMessages('timezones', locale); | |||
this.detectTimezone(); | |||
this.loadSystemConfigs(); // determine if we can show the registration link | |||
} | |||
}; | |||
</script> |
@@ -0,0 +1,54 @@ | |||
<template> | |||
<div> | |||
<h2>{{messages.form_label_title_login}}</h2> | |||
<form @submit.prevent="handleSubmit"> | |||
<div class="form-group"> | |||
<label for="name">{{messages.field_label_username}}</label> | |||
<input type="text" v-model="name" name="name" class="form-control" :class="{ 'is-invalid': submitted && !name }" /> | |||
<div v-show="submitted && !name" class="invalid-feedback">Name is required</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="password">{{messages.field_label_password}}</label> | |||
<input type="password" v-model="password" name="password" class="form-control" :class="{ 'is-invalid': submitted && !password }" /> | |||
<div v-show="submitted && !password" class="invalid-feedback">Password is required</div> | |||
</div> | |||
<div class="form-group"> | |||
<button class="btn btn-primary" :disabled="status.loggingIn">{{messages.button_label_login}}</button> | |||
<img v-show="status.loggingIn" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" /> | |||
<router-link v-if="configs && configs.allowRegistration" to="/register" class="btn btn-link">{{messages.button_label_register}}</router-link> | |||
</div> | |||
</form> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions } from 'vuex' | |||
export default { | |||
data () { | |||
return { | |||
name: '', | |||
password: '', | |||
submitted: false | |||
} | |||
}, | |||
computed: { | |||
...mapState('account', ['status']), | |||
...mapState('system', ['configs', 'messages']) | |||
}, | |||
created () { | |||
this.logout(); // reset login status | |||
}, | |||
methods: { | |||
...mapActions('account', ['login', 'logout']), | |||
...mapActions('system', ['loadSystemConfigs']), | |||
handleSubmit (e) { | |||
this.submitted = true; | |||
const { name, password } = this; | |||
if (name && password) { | |||
this.login({user: {name, password}, messages: this.messages, errors: this.errors}); | |||
} | |||
} | |||
} | |||
}; | |||
</script> |
@@ -0,0 +1,75 @@ | |||
<template> | |||
<div> | |||
<h2>{{messages.form_label_title_register}}</h2> | |||
<form @submit.prevent="handleSubmit"> | |||
<div class="form-group"> | |||
<label for="name">{{messages.field_label_username}}</label> | |||
<input type="text" v-model="user.name" v-validate="'required'" name="name" class="form-control" :class="{ 'is-invalid': submitted && errors.has('name') }" /> | |||
<div v-if="submitted && errors.has('name')" class="invalid-feedback">{{ errors.first('name') }}</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="password">{{messages.field_label_password}}</label> | |||
<input type="password" v-model="user.password" v-validate="{ required: true, min: 6 }" name="password" class="form-control" :class="{ 'is-invalid': submitted && errors.has('password') }" /> | |||
<div v-if="submitted && errors.has('password')" class="invalid-feedback">{{ errors.first('password') }}</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="type">{{messages.field_label_contactType}}</label> | |||
</div> | |||
<div class="form-group"> | |||
<label for="email">{{messages.field_label_email}}</label> | |||
<input type="text" v-model="user.contact.info" name="email" class="form-control" :class="{ 'is-invalid': submitted && errors.has('email') }" /> | |||
<div v-if="submitted && errors.has('email')" class="invalid-feedback">{{ errors.first('email') }}</div> | |||
</div> | |||
<div class="form-group"> | |||
<label for="user.contact.receiveInformationalMessages">{{messages.field_label_receiveInformationalMessages}}</label> | |||
<input type="checkbox" id="user.contact.receiveInformationalMessages" v-model="user.contact.receiveInformationalMessages"> | |||
</div> | |||
<div class="form-group"> | |||
<label for="user.contact.receivePromotionalMessages">{{messages.field_label_receiveInformationalMessages}}</label> | |||
<input type="checkbox" id="user.contact.receivePromotionalMessages" v-model="user.contact.receivePromotionalMessages"> | |||
</div> | |||
<div class="form-group"> | |||
<button class="btn btn-primary" :disabled="status.registering">{{messages.button_label_register}}</button> | |||
<img v-show="status.registering" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" /> | |||
<router-link to="/login" class="btn btn-link">{{messages.button_label_cancel}}</router-link> | |||
</div> | |||
</form> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions } from 'vuex' | |||
export default { | |||
data () { | |||
return { | |||
user: { | |||
name: '', | |||
password: '', | |||
contact: { | |||
type: 'email', | |||
info: '', | |||
receiveInformationalMessages: false, | |||
receivePromotionalMessages: false | |||
} | |||
}, | |||
submitted: false | |||
} | |||
}, | |||
computed: { | |||
...mapState('account', ['status']), | |||
...mapState('system', ['messages', 'countries']) | |||
}, | |||
methods: { | |||
...mapActions('account', ['register']), | |||
handleSubmit(e) { | |||
this.submitted = true; | |||
this.$validator.validate().then(valid => { | |||
if (valid) { | |||
this.register({user: this.user, messages: this.messages, errors: this.errors}); | |||
} | |||
}); | |||
} | |||
} | |||
}; | |||
</script> |
@@ -0,0 +1,15 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<title>Bubble</title> | |||
<link href="//netdna.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" /> | |||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous"> | |||
<style> | |||
a { cursor: pointer; } | |||
</style> | |||
</head> | |||
<body> | |||
<div id="app"></div> | |||
</body> | |||
</html> |
@@ -0,0 +1,32 @@ | |||
import Vue from 'vue'; | |||
import VeeValidate from 'vee-validate'; | |||
import VueSidebarMenu from 'vue-sidebar-menu' | |||
import vSelect from 'vue-select' | |||
// not sure what the best way is to include these icons | |||
// import { library } from '@fortawesome/fontawesome-svg-core' | |||
// import { faCoffee } from '@fortawesome/free-solid-svg-icons' | |||
// import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' | |||
// library.add(faCoffee); | |||
import { store } from './_store'; | |||
import { router } from './_helpers'; | |||
import App from './app/App'; | |||
// why can't i import this? | |||
// import 'vue-select/dist/vue-select.css'; | |||
Vue.use(VeeValidate); | |||
Vue.use(VueSidebarMenu); | |||
Vue.component('v-select', vSelect); | |||
Vue.config.productionTip = false; | |||
// not sure what the best way is to include these icons, we reference them programmatically via string resource/messages | |||
// Vue.component('font-awesome-icon', FontAwesomeIcon); | |||
new Vue({ | |||
el: '#app', | |||
router, | |||
store, | |||
render: h => h(App) | |||
}); |
@@ -0,0 +1,21 @@ | |||
MIT License | |||
Copyright (c) 2018 Jason Watmore | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. |
@@ -0,0 +1,5 @@ | |||
# vue-vuex-registration-login-example | |||
Vue + Vuex - User Registration and Login Tutorial & Example | |||
To see a demo and further details go to http://jasonwatmore.com/post/2018/07/14/vue-vuex-user-registration-and-login-tutorial-example |
@@ -0,0 +1,36 @@ | |||
var path = require('path'); | |||
var HtmlWebpackPlugin = require('html-webpack-plugin'); | |||
module.exports = { | |||
mode: 'development', | |||
resolve: { | |||
extensions: ['.js', '.vue'] | |||
}, | |||
module: { | |||
rules: [ | |||
{ | |||
test: /\.vue?$/, | |||
exclude: /(node_modules)/, | |||
use: 'vue-loader' | |||
}, | |||
{ | |||
test: /\.js?$/, | |||
exclude: /(node_modules)/, | |||
use: 'babel-loader' | |||
} | |||
] | |||
}, | |||
plugins: [new HtmlWebpackPlugin({ | |||
template: './src/index.html' | |||
})], | |||
devServer: { | |||
historyApiFallback: true | |||
}, | |||
externals: { | |||
// global app config object | |||
config: JSON.stringify({ | |||
// apiUrl: 'http://localhost:4000' | |||
apiUrl: '/api' | |||
}) | |||
} | |||
} |