@@ -10,10 +10,12 @@ | |||
"start": "webpack-dev-server --open" | |||
}, | |||
"dependencies": { | |||
"luxon": "^1.21.3", | |||
"qrcode": "^1.4.4", | |||
"safe-eval": "^0.4.1", | |||
"vee-validate": "^2.2.8", | |||
"vue": "^2.6.10", | |||
"vue-datetime": "^1.0.0-beta.11", | |||
"vue-router": "^3.0.6", | |||
"vue-select": "^3.4.0", | |||
"vue-sidebar-menu": "^4.4.3", | |||
@@ -12,6 +12,7 @@ import ActionPage from '../account/profile/ActionPage' | |||
import PolicyPage from '../account/profile/PolicyPage' | |||
import NotificationsPage from '../account/NotificationsPage' | |||
import ChangePasswordPage from '../account/profile/ChangePasswordPage' | |||
import SshKeysPage from '../account/profile/SshKeysPage' | |||
import NetworksPage from '../account/NetworksPage' | |||
import NewNetworkPage from '../account/NewNetworkPage' | |||
import NetworkPage from '../account/NetworkPage' | |||
@@ -49,6 +50,7 @@ export const router = new Router({ | |||
{ path: '/me/policy', component: PolicyPage }, | |||
{ path: '/me/action', component: ActionPage }, | |||
{ path: '/me/changePassword', component: ChangePasswordPage }, | |||
{ path: '/me/keys', component: SshKeysPage }, | |||
{ path: '/notifications', component: NotificationsPage }, | |||
{ | |||
path: '/bubbles', component: NetworksPage , | |||
@@ -161,5 +161,15 @@ export const util = { | |||
} | |||
return false; | |||
}; | |||
}, | |||
userHasLocale: function(user) { | |||
return !(typeof user === 'undefined' || user === null || !user.hasOwnProperty('locale') || user.locale === null || user.locale === '' || user.locale === 'detect'); | |||
}, | |||
jsLocale: function (user, detectedLocale) { | |||
const loc = util.userHasLocale(user) ? user.locale : detectedLocale; | |||
return loc === null ? null : loc.replace('_', '-').toLowerCase(); | |||
} | |||
}; |
@@ -13,6 +13,9 @@ export const userService = { | |||
addPolicyContactById, | |||
removePolicyContactByUuid, | |||
setLocale, | |||
addSshKeyByUserId, | |||
removeSshKeyByUserId, | |||
listSshKeysByUserId, | |||
updateUser, | |||
deleteUser, | |||
approveAction, | |||
@@ -120,6 +123,18 @@ function setLocale(locale, messages, errors) { | |||
} | |||
} | |||
function addSshKeyByUserId(userId, sshKey, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/keys`, util.putWithAuth(sshKey)).then(util.handleCrudResponse(messages, errors)); | |||
} | |||
function removeSshKeyByUserId(userId, sshKeyId, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/keys/${sshKeyId}`, util.deleteWithAuth()).then(util.handleCrudResponse(messages, errors)); | |||
} | |||
function listSshKeysByUserId(userId, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/keys`, util.getWithAuth()).then(util.handleCrudResponse(messages, errors)); | |||
} | |||
function updateUser(user, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${user.uuid}`, util.postWithAuth(user)).then(util.handleCrudResponse(messages, errors)); | |||
} | |||
@@ -46,7 +46,23 @@ function evalInContext(vue, string) { | |||
String.prototype.parseMessage = function (vue, ctx) { | |||
const context = (typeof ctx !== 'undefined' && ctx !== null) ? Object.assign(vue, ctx) : vue; | |||
return this ? ''+this.replace(/{{[\w\._]*?}}/g, match => { | |||
return this ? ''+this.replace(/{{[\w][\w\._]*?}}/g, match => { | |||
const expression = match.slice(2, -2); | |||
return evalInContext(context, expression) | |||
}) : ''; | |||
}; | |||
String.prototype.parseDateMessage = function (millis, messages) { | |||
const date = new Date(millis); | |||
const context = { | |||
YYYY: date.getFullYear(), | |||
MMM: messages['label_date_month_'+date.getMonth()], | |||
M: messages['label_date_month_short_'+date.getMonth()], | |||
EEE: messages['label_date_day_'+date.getDay()], | |||
E: messages['label_date_day_short_'+date.getDay()], | |||
d: date.getDate() | |||
}; | |||
return this ? ''+this.replace(/{{[\w]+?}}/g, match => { | |||
const expression = match.slice(2, -2); | |||
return evalInContext(context, expression) | |||
}) : ''; | |||
@@ -5,14 +5,17 @@ import { util } from '../_helpers'; | |||
const state = { | |||
loading: { | |||
users: false, user: false, updating: false, deleting: false, | |||
policy: false, updatingPolicy: false, addPolicyContact: false, removePolicyContact: false | |||
policy: false, updatingPolicy: false, addPolicyContact: false, removePolicyContact: false, | |||
listSshKeys: false, addSshKey: false, removeSshKey: false | |||
}, | |||
errors: {}, | |||
users: null, | |||
user: null, | |||
policy: {}, | |||
contact: null, | |||
authenticator: {} | |||
authenticator: {}, | |||
sshKey: null, | |||
sshKeys: [] | |||
}; | |||
export const CONTACT_TYPE_AUTHENTICATOR = 'authenticator'; | |||
@@ -113,6 +116,33 @@ const actions = { | |||
); | |||
}, | |||
addSshKeyByUserId({ commit }, {userId, sshKey, messages, errors}) { | |||
commit('addSshKeyByUserIdRequest'); | |||
userService.addSshKeyByUserId(userId, sshKey, messages, errors) | |||
.then( | |||
key => commit('addSshKeyByUserIdSuccess', key), | |||
error => commit('addSshKeyByUserIdFailure', error) | |||
); | |||
}, | |||
removeSshKeyByUserId({ commit }, {userId, sshKeyId, messages, errors}) { | |||
commit('removeSshKeyByUserIdRequest'); | |||
userService.removeSshKeyByUserId(userId, sshKeyId, messages, errors) | |||
.then( | |||
ok => commit('removeSshKeyByUserIdSuccess', sshKeyId), | |||
error => commit('removeSshKeyByUserIdFailure', error) | |||
); | |||
}, | |||
listSshKeysByUserId({ commit }, {userId, messages, errors}) { | |||
commit('listSshKeysByUserIdRequest'); | |||
userService.listSshKeysByUserId(userId, messages, errors) | |||
.then( | |||
sshKeys => commit('listSshKeysByUserIdSuccess', sshKeys), | |||
error => commit('listSshKeysByUserIdFailure', error) | |||
); | |||
}, | |||
delete({ commit }, {userId, messages, errors}) { | |||
commit('deleteRequest', userId); | |||
userService.deleteUser(userId, messages, errors) | |||
@@ -217,6 +247,44 @@ const mutations = { | |||
state.errors.update = error; | |||
}, | |||
addSshKeyByUserIdRequest(state) { | |||
state.loading.addSshKey = true; | |||
}, | |||
addSshKeyByUserIdSuccess(state, sshKey) { | |||
state.loading.addSshKey = false; | |||
state.sshKey = sshKey; | |||
state.sshKeys.push(sshKey); | |||
}, | |||
addSshKeyByUserIdFailure(state, error) { | |||
state.loading.addSshKey = false; | |||
state.errors.sshKey = error; | |||
}, | |||
removeSshKeyByUserIdRequest(state) { | |||
state.loading.removeSshKey = true; | |||
}, | |||
removeSshKeyByUserIdSuccess(state, sshKeyId) { | |||
state.loading.removeSshKey = false; | |||
state.sshKey = null; | |||
state.sshKeys = state.sshKeys.filter(function(k) { return k.uuid !== sshKeyId; }) | |||
}, | |||
removeSshKeyByUserIdFailure(state, error) { | |||
state.loading.removeSshKey = false; | |||
state.errors.sshKey = error; | |||
}, | |||
listSshKeysByUserIdRequest(state) { | |||
state.loading.listSshKeys = true; | |||
}, | |||
listSshKeysByUserIdSuccess(state, sshKeys) { | |||
state.loading.listSshKeys = false; | |||
state.sshKeys = sshKeys; | |||
}, | |||
listSshKeysByUserIdFailure(state, error) { | |||
state.loading.listSshKeys = false; | |||
state.errors.sshKey = error; | |||
}, | |||
deleteRequest(state, id) { | |||
// todo: use proper delete API | |||
// add 'deleting:true' property to user being deleted | |||
@@ -254,9 +254,9 @@ | |||
</tbody> | |||
</table> | |||
<hr/> | |||
<h4>{{messages.form_title_account_add_contact}}</h4> | |||
<form @submit.prevent="addContact"> | |||
<hr/> | |||
<div class="form-group"> | |||
<label htmlFor="contactType">{{messages.field_label_policy_contact_type}}</label> | |||
<select v-model="newContact.type" name="contactType" class="form-control"> | |||
@@ -7,6 +7,8 @@ | |||
<hr/> | |||
<router-link to="/me/policy">{{messages.link_label_account_policy}}</router-link> | |||
<hr/> | |||
<router-link to="/me/keys">{{messages.link_label_account_ssh_keys}}</router-link> | |||
<hr/> | |||
<form @submit.prevent="handleSubmit"> | |||
<div class="form-group"> | |||
@@ -0,0 +1,157 @@ | |||
<template> | |||
<div> | |||
<h4>{{messages.form_title_ssh_keys}}</h4> | |||
<table border="1"> | |||
<thead> | |||
<tr> | |||
<td>{{messages.field_label_ssh_key_name}}</td> | |||
<!-- <td>{{messages.field_label_ssh_key_public_key_hash}}</td>--> | |||
<td>{{messages.field_label_ssh_key_expiration}}</td> | |||
<td v-if="user.admin">{{messages.field_label_ssh_key_install}}</td> | |||
<td></td> | |||
</tr> | |||
</thead> | |||
<tbody v-if="sshKeys"> | |||
<tr v-for="key in sshKeys"> | |||
<td>{{key.name}}</td> | |||
<!-- <td><small>{{key.sshPublicKeyHash}}</small></td>--> | |||
<td nowrap="nowrap"> | |||
<span v-if="key.expiration">{{messages.date_format_ssh_key_expiration.parseDateMessage(key.expiration, messages)}}</span> | |||
<span v-else>{{messages.message_ssh_key_no_expiration}}</span> | |||
</td> | |||
<td v-if="user.admin"> | |||
<span v-if="key.installSshKey">{{messages.message_true}}</span> | |||
<span v-else>{{messages.message_false}}</span> | |||
</td> | |||
<td> | |||
<i @click="removeSshKey(key.uuid)" aria-hidden="true" :class="messages.button_label_remove_ssh_key_icon" :title="messages.button_label_remove_ssh_key"></i> | |||
<span class="sr-only">{{messages.button_label_remove_ssh_key}}</span> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
<hr/> | |||
<form @submit.prevent="addSshKey"> | |||
<h4>{{messages.form_title_add_ssh_key}}</h4> | |||
<div class="form-group"> | |||
<label htmlFor="name">{{messages.field_label_ssh_key_name}}</label> | |||
<input v-validate="'required'" v-model="name" name="name" class="form-control"/> | |||
<div v-if="submitted && errors.has('name')" class="invalid-feedback d-block">{{ errors.first('name') }}</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="sshPublicKey">{{messages.field_label_ssh_key_public_key}}</label> | |||
<textarea v-validate="'required'" v-model="sshPublicKey" name="name" class="form-control" cols="50"></textarea> | |||
<div v-html="messages.field_description_ssh_key_public_key"></div> | |||
<div v-if="submitted && errors.has('sshPublicKey')" class="invalid-feedback d-block">{{ errors.first('sshPublicKey') }}</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="expiration">{{messages.field_label_ssh_key_expiration}}</label> | |||
<datetime v-model="expiration" input-id="expiration" input-class="form-control" :min-datetime="minExpiration" :phrases="dateControlPhrases" :zone="timezone" :week-start="parseInt(messages.datecontrol_weekstart)"></datetime> | |||
<div v-html="messages.field_description_ssh_key_expiration"></div> | |||
<div v-if="submitted && errors.has('expiration')" class="invalid-feedback d-block">{{ errors.first('expiration') }}</div> | |||
</div> | |||
<div class="form-group" v-if="user.admin"> | |||
<label htmlFor="installSshKey">{{messages.field_label_ssh_key_install}}</label> | |||
<input v-model="installSshKey" name="installSshKey" class="form-control" type="checkbox"/> | |||
<div v-if="submitted && errors.has('installSshKey')" class="invalid-feedback d-block">{{ errors.first('installSshKey') }}</div> | |||
</div> | |||
<div class="form-group"> | |||
<button class="btn btn-primary" :disabled="loading() || !newKeyValid">{{messages.button_label_add_ssh_key}}</button> | |||
<img v-show="loading()" 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, mapGetters } from 'vuex' | |||
import { util } from '../../_helpers' | |||
import { Settings } from 'luxon' | |||
export default { | |||
data() { | |||
return { | |||
submitted: false, | |||
user: util.currentUser(), | |||
name: null, | |||
expiration: null, | |||
sshPublicKey: null, | |||
installSshKey: false, | |||
minExpiration: (new Date()).toISOString(), | |||
timezone: null | |||
}; | |||
}, | |||
computed: { | |||
...mapState('system', ['messages', 'detectedTimezone', 'detectedLocale']), | |||
...mapState('users', ['sshKeys']), | |||
newKeyValid () { | |||
return (this.name !== null && this.name !== '') | |||
&& (this.sshPublicKey !== null && this.sshPublicKey !== '' && this.sshPublicKey.startsWith('ssh-rsa ')); | |||
}, | |||
dateControlPhrases () { | |||
return {ok: this.messages.message_datecontrol_ok, cancel: this.messages.message_datecontrol_cancel}; | |||
} | |||
}, | |||
methods: { | |||
...mapActions('system', ['detectTimezone', 'detectLocale']), | |||
...mapActions('users', ['addSshKeyByUserId', 'removeSshKeyByUserId', 'listSshKeysByUserId']), | |||
...mapGetters('users', ['loading']), | |||
addSshKey (e) { | |||
this.errors.clear(); | |||
this.submitted = true; | |||
this.addSshKeyByUserId({ | |||
userId: this.user.uuid, | |||
sshKey: { | |||
name: this.name, | |||
sshPublicKey: this.sshPublicKey, | |||
expirationISO8601: this.expiration, | |||
installSshKey: this.user.admin ? this.installSshKey : null | |||
}, | |||
messages: this.messages, | |||
errors: this.errors | |||
}); | |||
}, | |||
removeSshKey (keyId) { | |||
this.removeSshKeyByUserId({ | |||
userId: this.user.uuid, | |||
sshKeyId: keyId, | |||
messages: this.messages, | |||
errors: this.errors | |||
}); | |||
}, | |||
}, | |||
watch: { | |||
detectedLocale (locale) { | |||
if (locale) { | |||
Settings.defaultLocale = util.jsLocale(this.user, locale); | |||
} | |||
}, | |||
detectedTimezone (tz) { | |||
if (tz) this.timezone = tz.timeZoneId; | |||
} | |||
}, | |||
created () { | |||
if (this.detectedTimezone === null || !this.detectedTimezone.hasOwnProperty('timeZoneId')) { | |||
this.detectTimezone(); | |||
} else { | |||
this.timezone = this.detectedTimezone.timeZoneId; | |||
} | |||
const user = util.currentUser(); | |||
if (util.userHasLocale(user)) { | |||
Settings.defaultLocale = util.jsLocale(this.user, null); | |||
} else if (this.detectedLocale === null) { | |||
this.detectLocale(); | |||
} else { | |||
Settings.defaultLocale = util.jsLocale(null, this.detectedLocale); | |||
} | |||
this.listSshKeysByUserId({userId: user.uuid, messages: this.messages, errors: this.errors}); | |||
} | |||
}; | |||
</script> |
@@ -3,8 +3,9 @@ | |||
<head> | |||
<meta charset="UTF-8"> | |||
<title>Bubble</title> | |||
<link href="//netdna.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" /> | |||
<link href="/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"> | |||
<link rel="stylesheet" href="/vue-datetime.css"> | |||
<style> | |||
a { cursor: pointer; } | |||
@@ -2,6 +2,7 @@ import Vue from 'vue'; | |||
import VeeValidate from 'vee-validate'; | |||
import VueSidebarMenu from 'vue-sidebar-menu' | |||
import vSelect from 'vue-select' | |||
import { Datetime } from 'vue-datetime'; | |||
// not sure what the best way is to include these icons | |||
// import { library } from '@fortawesome/fontawesome-svg-core' | |||
@@ -19,6 +20,7 @@ import App from './app/App'; | |||
Vue.use(VeeValidate); | |||
Vue.use(VueSidebarMenu); | |||
Vue.component('v-select', vSelect); | |||
Vue.component('datetime', Datetime); | |||
Vue.config.productionTip = false; | |||
// not sure what the best way is to include these icons, we reference them programmatically via string resource/messages | |||