Quellcode durchsuchen

first commit

pull/1/head
Jonathan Cobb vor 5 Jahren
Commit
6cb7ebd243
39 geänderte Dateien mit 9837 neuen und 0 gelöschten Zeilen
  1. +38
    -0
      .gitignore
  2. +25
    -0
      README.md
  3. +7870
    -0
      package-lock.json
  4. +36
    -0
      package.json
  5. +95
    -0
      src/_helpers/api-util.js
  6. +2
    -0
      src/_helpers/index.js
  7. +53
    -0
      src/_helpers/router.js
  8. +20
    -0
      src/_services/accountPlan.service.js
  9. +15
    -0
      src/_services/domain.service.js
  10. +15
    -0
      src/_services/footprint.service.js
  11. +7
    -0
      src/_services/index.js
  12. +15
    -0
      src/_services/network.service.js
  13. +15
    -0
      src/_services/plan.service.js
  14. +37
    -0
      src/_services/system.service.js
  15. +95
    -0
      src/_services/user.service.js
  16. +104
    -0
      src/_store/account.module.js
  17. +88
    -0
      src/_store/accountPlans.module.js
  18. +38
    -0
      src/_store/alert.module.js
  19. +60
    -0
      src/_store/domains.module.js
  20. +60
    -0
      src/_store/footprints.module.js
  21. +50
    -0
      src/_store/index.js
  22. +90
    -0
      src/_store/networks.module.js
  23. +84
    -0
      src/_store/plans.module.js
  24. +128
    -0
      src/_store/system.module.js
  25. +114
    -0
      src/_store/users.module.js
  26. +33
    -0
      src/account/HomePage.vue
  27. +28
    -0
      src/account/NetworkPage.vue
  28. +61
    -0
      src/account/NetworksPage.vue
  29. +140
    -0
      src/account/NewNetworkPage.vue
  30. +93
    -0
      src/account/ProfilePage.vue
  31. +42
    -0
      src/admin/AccountsPage.vue
  32. +48
    -0
      src/app/App.vue
  33. +54
    -0
      src/auth/LoginPage.vue
  34. +75
    -0
      src/auth/RegisterPage.vue
  35. +15
    -0
      src/index.html
  36. +32
    -0
      src/index.js
  37. +21
    -0
      vue-vuex-registration-login-example-docs/LICENSE
  38. +5
    -0
      vue-vuex-registration-login-example-docs/README.md
  39. +36
    -0
      webpack.config.js

+ 38
- 0
.gitignore Datei anzeigen

@@ -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
*~

+ 25
- 0
README.md Datei anzeigen

@@ -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.

+ 7870
- 0
package-lock.json
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 36
- 0
package.json Datei anzeigen

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

+ 95
- 0
src/_helpers/api-util.js Datei anzeigen

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

+ 2
- 0
src/_helpers/index.js Datei anzeigen

@@ -0,0 +1,2 @@
export * from './router';
export * from './api-util';

+ 53
- 0
src/_helpers/router.js Datei anzeigen

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

+ 20
- 0
src/_services/accountPlan.service.js Datei anzeigen

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

+ 15
- 0
src/_services/domain.service.js Datei anzeigen

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

+ 15
- 0
src/_services/footprint.service.js Datei anzeigen

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

+ 7
- 0
src/_services/index.js Datei anzeigen

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

+ 15
- 0
src/_services/network.service.js Datei anzeigen

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

+ 15
- 0
src/_services/plan.service.js Datei anzeigen

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

+ 37
- 0
src/_services/system.service.js Datei anzeigen

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

+ 95
- 0
src/_services/user.service.js Datei anzeigen

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

+ 104
- 0
src/_store/account.module.js Datei anzeigen

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

+ 88
- 0
src/_store/accountPlans.module.js Datei anzeigen

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

+ 38
- 0
src/_store/alert.module.js Datei anzeigen

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

+ 60
- 0
src/_store/domains.module.js Datei anzeigen

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

+ 60
- 0
src/_store/footprints.module.js Datei anzeigen

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

+ 50
- 0
src/_store/index.js Datei anzeigen

@@ -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)
}) : '';
};

+ 90
- 0
src/_store/networks.module.js Datei anzeigen

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

+ 84
- 0
src/_store/plans.module.js Datei anzeigen

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

+ 128
- 0
src/_store/system.module.js Datei anzeigen

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

+ 114
- 0
src/_store/users.module.js Datei anzeigen

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

+ 33
- 0
src/account/HomePage.vue Datei anzeigen

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

+ 28
- 0
src/account/NetworkPage.vue Datei anzeigen

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

+ 61
- 0
src/account/NetworksPage.vue Datei anzeigen

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

+ 140
- 0
src/account/NewNetworkPage.vue Datei anzeigen

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

+ 93
- 0
src/account/ProfilePage.vue Datei anzeigen

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

+ 42
- 0
src/admin/AccountsPage.vue Datei anzeigen

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

+ 48
- 0
src/app/App.vue Datei anzeigen

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

+ 54
- 0
src/auth/LoginPage.vue Datei anzeigen

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

+ 75
- 0
src/auth/RegisterPage.vue Datei anzeigen

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

+ 15
- 0
src/index.html Datei anzeigen

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

+ 32
- 0
src/index.js Datei anzeigen

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

+ 21
- 0
vue-vuex-registration-login-example-docs/LICENSE Datei anzeigen

@@ -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.

+ 5
- 0
vue-vuex-registration-login-example-docs/README.md Datei anzeigen

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

+ 36
- 0
webpack.config.js Datei anzeigen

@@ -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'
})
}
}

Laden…
Abbrechen
Speichern