소스 검색

add ssh keys page, add support for dates

pull/1/head
Jonathan Cobb 4 년 전
부모
커밋
110d70bde5
14개의 변경된 파일2398개의 추가작업 그리고 5개의 파일을 삭제
  1. +2
    -0
      package.json
  2. +2
    -0
      src/_helpers/router.js
  3. +10
    -0
      src/_helpers/util.js
  4. +15
    -0
      src/_services/user.service.js
  5. +17
    -1
      src/_store/index.js
  6. +70
    -2
      src/_store/users.module.js
  7. +1
    -1
      src/account/profile/PolicyPage.vue
  8. +2
    -0
      src/account/profile/ProfilePage.vue
  9. +157
    -0
      src/account/profile/SshKeysPage.vue
  10. +2
    -1
      src/index.html
  11. +2
    -0
      src/index.js
  12. +7
    -0
      src/public/bootstrap.min.css
  13. +5
    -0
      src/public/fontawesome-5.3.1-all.css
  14. +2106
    -0
      src/public/vue-datetime.css

+ 2
- 0
package.json 파일 보기

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


+ 2
- 0
src/_helpers/router.js 파일 보기

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


+ 10
- 0
src/_helpers/util.js 파일 보기

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

};

+ 15
- 0
src/_services/user.service.js 파일 보기

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


+ 17
- 1
src/_store/index.js 파일 보기

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


+ 70
- 2
src/_store/users.module.js 파일 보기

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


+ 1
- 1
src/account/profile/PolicyPage.vue 파일 보기

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


+ 2
- 0
src/account/profile/ProfilePage.vue 파일 보기

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


+ 157
- 0
src/account/profile/SshKeysPage.vue 파일 보기

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

+ 2
- 1
src/index.html 파일 보기

@@ -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
- 0
src/index.js 파일 보기

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


+ 7
- 0
src/public/bootstrap.min.css
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


+ 5
- 0
src/public/fontawesome-5.3.1-all.css
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


+ 2106
- 0
src/public/vue-datetime.css
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
파일 보기


불러오는 중...
취소
저장