Commit f5b9b329 by Heechul Kim

quasar sources added

parent f82bf849
<template>
<q-layout>
<div slot="header" class="toolbar purple">
<q-toolbar-title :padding="0">
UDAM v{{porchVersion}}
</q-toolbar-title>
<q-tabs slot="navigation" class="purple">
<q-tab
v-if="this.$store.state.login"
icon="create_new_folder"
route="/section"
exact
replace
>그룹
</q-tab>
<q-tab
v-if="this.$store.state.login"
icon="computer"
route="/machine"
exact
replace
>서버
</q-tab>
<q-tab
v-if="this.$store.state.login"
icon="apps"
route="/app"
exact
replace
>애플리케이션
</q-tab>
<q-tab
v-if="this.$store.state.login && this.$store.state.role === 'admin'"
icon="person_outline"
route="/account"
exact
replace
>사용자
</q-tab>
<q-tab
v-if="this.$store.state.login"
icon="exit_to_app"
route="/logout"
exact
replace
>로그아웃
</q-tab>
<q-tab
v-else
icon="person"
route="/login"
exact
replace
>로그인
</q-tab>
<!--<q-tab icon="home" route="/" exact replace>Home</q-tab>-->
</q-tabs>
</div>
<!-- Don't drop "q-app" class -->
<div class="layout-view">
<div class="layout-padding">
<div id="q-app">
<router-view></router-view>
</div>
</div>
</div>
<div slot="footer">
</div>
</q-layout>
</template>
<script>
export default {
name: 'app',
data () {
return {
porchVersion: '0.0.1'
}
},
methods: {
updateMenu: function () {
this.login = this.$store.state.login
}
},
updated () {
this.updateMenu()
},
mounted () {
this.updateMenu()
}
}
</script>
<style></style>
<template>
<div class="error-page window-height window-width bg-light column items-center">
<div class="error-code bg-primary flex items-center justify-center">
404
</div>
<div>
<div class="error-card card bg-white column items-center justify-center">
<i class="text-grey-5">error_outline</i>
<p class="caption text-center">Oops. Nothing here...</p>
<p class="text-center group">
<button v-if="canGoBack" class="grey push small" @click="goBack">
<i class="on-left">keyboard_arrow_left</i>
Go back
</button>
<router-link to="/">
<button class="grey push small">
Go home
<i class="on-right">home</i>
</button>
</router-link>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
canGoBack: window.history.length > 1
}
},
methods: {
goBack () {
window.history.go(-1)
}
}
}
</script>
<style lang="stylus">
.error-page
.error-code
height 50vh
width 100%
padding-top 15vh
font-size 30vmax
color rgba(255, 255, 255, .2)
overflow hidden
.error-card
margin-top -25px
width 90vw
max-width 600px
padding 50px
i
font-size 5rem
</style>
<template>
<div></div>
</template>
<script>
export default {
name: 'index',
data () {
return {
}
},
methods: {
redirect: function () {
if (this.$store.state.login) {
this.$router.push('/section')
}
else {
this.$router.push('/login')
}
}
},
mounted () {
this.redirect()
}
}
</script>
<style>
</style>
<template>
<div class="login-page bg-light column items-center">
<div class="login-logo bg-purple flex items-center justify-center">
UDAM 로그인
</div>
<div>
<div class="login-card card bg-white column items-center justify-center">
<div class="column">
<div class="stacked-label width-1of3">
<input
type="text"
v-model="name"
ref="name"
placeholder="사용자 ID를 입력하세요."
>
<label>사용자 ID</label>
</div>
<div class="stacked-label">
<input
type="password"
v-model="pass"
v-on:keyup.enter="auth"
placeholder="비밀번호를 입력하세요."
>
<label>비밀번호</label>
</div>
<div>
<button
v-on:keyup.enter="submit"
class="purple"
@click="auth"
>로그인
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { API_URL } from 'config'
import { Toast } from 'quasar'
export default {
name: 'login',
data () {
return {
name: '',
pass: ''
}
},
methods: {
auth: function () {
const url = API_URL + '/account/login'
const dReqBody = {
name: this.name,
pass: this.pass
}
const headers = {
headers: {
// 'Access-Control-Allow-Origin': '*'
}
}
this.$http.post(url, dReqBody, headers).then(response => {
// save token cookie
// this.$cookie.set('token', response.body.token)
// this.$cookie.set('refresh_token', response.body.refresh_token)
// this.$cookie.set('role', response.body.role)
// this.$cookie.set('name', response.body.name)
// state update
this.$store.commit('loggedIn', true)
this.$store.commit('setName', this.name)
this.$store.commit('setRole', response.body.role)
this.$store.commit('setToken', response.body.token)
this.$store.commit('setRefreshToken', response.body.refresh_token)
this.$router.push('/section')
}, response => {
console.log(response.body)
Toast.create.negative({
html: '로그인 실패: ' + response.body.error,
timeout: 5000
})
this.name = ''
this.pass = ''
this.$refs.name.focus()
})
}
},
mounted () {
this.$refs.name.focus()
}
}
</script>
<style lang="stylus">
.login-page
.login-logo
height 12vh
width 60%
padding-top 1vh
font-size 5vmax
color rgba(255, 255, 255, .7)
overflow hidden
.login-card
margin-top 10px
width 90vw
max-width 600px
padding 20px
</style>
<template>
<div>로그아웃 되었습니다.</div>
</template>
<script>
export default {
name: 'logout',
data () {
return {
}
},
methods: {
logout: function () {
// delete token cookie
// this.$cookie.delete('token')
this.$store.commit('loggedIn', false)
this.$store.commit('setName', '')
this.$store.commit('setRole', '')
this.$store.commit('setToken', '')
this.$store.commit('setRefreshToken', '')
this.$router.push('/')
}
},
mounted () {
this.logout()
}
}
</script>
<style>
</style>
<template>
<div class="column">
<h6>
사용자 생성
</h6>
<p></p>
<form>
<div class="stacked-label">
<label>아이디</label>
<input
type="text"
v-model="form.name"
ref="name"
class="full-width"
:class="{'has-error': $v.form.name.$error}"
placeholder="사용자 ID를 입력하세요."
>
<span
class="text-negative"
v-if="!$v.form.name.required">
사용자 ID는 필수 입력 항목입니다.
</span>
<span
class="text-negative"
v-if="!$v.form.name.minLength">
사용자 ID는 3 문자 이상이어야 합니다.
</span>
<span
class="text-negative"
v-if="!$v.form.name.alphaNum">
사용자 ID는 알파벳과 숫자의 조합이어야 합니다.
</span>
</div>
<p></p>
<div class="stacked-label">
<label>비밀번호 (알파벳, 숫자, 특수문자 조합. 8자 이상)</label>
<input
type="password"
size="50"
v-model="form.pass"
ref="pass"
placeholder="비밀번호를 입력하세요."
:class="{'has-error': $v.form.pass.$error}"
>
</div>
<span
class="text-negative"
v-if="!$v.form.pass.required"
> 비밀번호는 필수 입력 항목입니다.
</span>
<span
class="text-negative"
v-if="!$v.form.pass.minLength"
> 비밀번호는 8자 이상이어야 합니다.
</span>
<p></p>
<div class="stacked-label">
<label>비밀번호 재입력</label>
<input
type="password"
size="50"
v-model="form.pass2"
ref="pass2"
placeholder="비밀번호를 재입력하세요."
>
</div>
<span
class="text-negative"
v-if="!$v.form.pass2.sameAsPassword"
> 비밀번호가 일치하지 않습니다.
</span>
<p></p>
<div class="stacked-label">
<label>성함</label>
<input
type="text"
size="50"
v-model="form.cn"
ref="cn"
class="full-width"
placeholder="성함을 입력하세요."
>
</div>
<span
class="text-negative"
v-if="!$v.form.cn.required"
> 사용자 성함은 필수 입력 항목입니다.
</span>
<p></p>
<div class="stacked-label">
<label>상태</label>
<p></p>
<q-select
type="radio"
v-model="form.state"
:options="form.stateOptions"
>
</q-select>
</div>
<span
class="text-negative"
v-if="!$v.form.state.required"
> 사용자 상태는 필수 입력 항목입니다.
</span>
<p></p>
<div class="stacked-label">
<label>만료일 (기본 90일 후)</label>
<p></p>
<q-datetime
type="date"
:min="min"
v-model="form.expireAt"
>
</q-select>
</div>
<p></p>
<div class="stacked-label">
<input
type="text"
size="50"
v-model="form.desc"
ref="desc"
class="full-width"
placeholder="사용자 설명을 입력하세요."
>
<label>설명</label>
</div>
<p></p>
<div>
<button class="purple" @click="submit">
생성
</button>
</div>
</form>
</div>
</template>
<script>
import { API_URL, checkStrength } from 'config'
import { Toast } from 'quasar'
import { sameAs, required, minLength, alphaNum } from 'vuelidate/lib/validators'
import moment from 'moment'
export default {
name: 'userCreate',
data () {
return {
min: moment(new Date().toISOString()).format(),
form: {
name: '',
pass: '',
pass2: '',
cn: '',
state: 'Enabled',
stateOptions: [
{ label: 'Enabled', value: 'Enabled' },
{ label: 'Disabled', value: 'Disabled' },
{ label: 'Locked', value: 'Locked' }
],
expireAt: moment(new Date().toISOString()).add(90, 'days').format(),
desc: ''
}
}
},
validations: {
form: {
name: {
required,
alphaNum,
minLength: minLength(3)
},
pass: {
required,
minLength: minLength(8)
},
pass2: {
sameAsPassword: sameAs('pass')
},
cn: {
required
},
state: {
required
},
expireAt: {
required
}
}
},
methods: {
submit: function () {
if (!this.form.name) {
Toast.create.negative('사용자 ID는 필수 입력 항목입니다..')
return
}
if (checkStrength(this.form.pass) < 3) {
Toast.create.negative('비밀번호가 보안규약에 위배됩니다.')
return
}
if (this.form.pass !== this.form.pass2) {
Toast.create.negative('비밀번호가 일치하지 않습니다.')
return
}
if (!this.form.cn) {
Toast.create.negative('사용자 성함은 필수 입력 항목입니다..')
return
}
if (!this.form.state) {
Toast.create.negative('사용자 상태는 필수 입력 항목입니다..')
return
}
if (!this.form.expireAt) {
Toast.create.negative('만료일은 필수 입력 항목입니다..')
return
}
const payload = {
name: this.form.name,
pass: this.form.pass,
cn: this.form.cn,
state: this.form.state,
expireAt: this.form.expireAt,
desc: this.form.desc
}
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/account'
this.$http.post(url, payload, headers).then(response => {
this.$router.push('/account')
}, response => {
Toast.create.negative('사용자 생성 실패')
this.$refs.name.focus()
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
}
}
</script>
<style>
</style>
<template>
<div class="column">
<h6>
사용자 수정
</h6>
<p></p>
<form>
<div class="stacked-label">
<label>아이디</label>
<input
type="text"
v-model="form.name"
ref="name"
class="full-width"
readonly
>
<span
class="text-negative"
v-if="!$v.form.name.required">
사용자 ID는 필수 입력 항목입니다.
</span>
<span
class="text-negative"
v-if="!$v.form.name.minLength">
사용자 ID는 3 문자 이상이어야 합니다.
</span>
<span
class="text-negative"
v-if="!$v.form.name.alphaNum">
사용자 ID는 알파벳과 숫자의 조합이어야 합니다.
</span>
</div>
<p></p>
<div class="stacked-label">
<label>성함</label>
<input
type="text"
size="50"
v-model="form.cn"
ref="cn"
class="full-width"
placeholder="성함을 입력하세요."
>
<span
class="text-negative"
v-if="!$v.form.cn.required">
성함은 필수 입력 항목입니다.
</span>
</div>
<p></p>
<div class="stacked-label">
<label>상태</label>
<p></p>
<q-select
type="radio"
v-model="form.state"
:options="form.stateOptions"
>
</q-select>
<span
class="text-negative"
v-if="!$v.form.state.required">
상태는 필수 입력 항목입니다.
</span>
</div>
<p></p>
<div class="stacked-label">
<label>만료일</label>
<p></p>
<q-datetime
type="date"
:min="min"
v-model="form.expireAt"
>
</q-select>
<span
class="text-negative"
v-if="!$v.form.expireAt.required">
만료일은 필수 입력 항목입니다.
</span>
</div>
<p></p>
<div class="stacked-label">
<input
type="text"
size="50"
v-model="form.desc"
ref="desc"
class="full-width"
placeholder="사용자 설명을 입력하세요."
>
<label>설명</label>
</div>
<p></p>
<div>
<button class="purple" @click="submit">
수정
</button>
</div>
</form>
</div>
</template>
<script>
import { API_URL } from 'config'
import { Toast } from 'quasar'
import { required, minLength, alphaNum } from 'vuelidate/lib/validators'
import moment from 'moment'
export default {
name: 'userCreate',
data () {
return {
min: moment(new Date().toISOString()).format(),
form: {
name: this.$route.params.name,
cn: '',
state: '',
stateOptions: [
{ label: 'Enabled', value: 'Enabled' },
{ label: 'Disabled', value: 'Disabled' },
{ label: 'Locked', value: 'Locked' }
],
expireAt: '',
desc: ''
}
}
},
validations: {
form: {
name: {
required,
alphaNum,
minLength: minLength(3)
},
cn: {
required
},
state: {
required
},
expireAt: {
required
}
}
},
methods: {
submit: function () {
const payload = {
name: this.form.name,
cn: this.form.cn,
state: this.form.state,
expireAt: this.form.expireAt,
desc: this.form.desc
}
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/account' + '/' + this.form.name
this.$http.patch(url, payload, headers).then(response => {
this.$router.push('/account')
}, response => {
Toast.create.negative('사용자 수정 실패')
})
},
loadData: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
let url = API_URL + '/account' + '/' + this.$route.params.name
this.$http.get(url, headers).then(response => {
let rep = response.body
this.form.cn = rep.cn ? rep.cn : ''
this.form.state = rep.state ? rep.state : ''
this.form.expireAt = rep.expireAt ? rep.expireAt : ''
this.form.desc = rep.desc ? rep.desc : ''
}, response => {
Toast.create.negative('사용자 정보 수신 실패')
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
this.loadData()
}
}
</script>
<style>
</style>
<template>
<div>
<button class="purple small" @click="goToCreate()">
<i>add</i>
생성
<q-tooltip>사용자 생성</q-tooltip>
</button>
<q-data-table
:data="data"
:config="config"
:columns="columns"
>
<template slot="col-functions" scope="cell">
<button
class="purple small circular"
@click="getUserInfo(cell.row.name)"
>
<i>info</i>
<q-tooltip>사용자 상세보기: {{ cell.row.name }}</q-tooltip>
</button>
<button
class="purple small circular"
@click="goToEdit(cell.row.name)"
>
<i>edit</i>
<q-tooltip>사용자 수정: {{ cell.row.name }}</q-tooltip>
</button>
<button
class="purple small circular"
@click="goToPassword(cell.row.name)"
>
<i>lock</i>
<q-tooltip>사용자 비밀번호 변경: {{ cell.row.name }}</q-tooltip>
</button>
<button
class="purple small circular"
@click="confirmDelete(cell.row.name)"
>
<i>delete</i>
<q-tooltip>사용자 삭제: {{ cell.row.name }}</q-tooltip>
</button>
</template>
</q-data-table>
<q-modal
ref="infoModal" :content-css="{minWidth: '50vw', minHeight: '60vh'}"
>
<q-layout>
<div slot="header" class="toolbar purple">
<button @click="$refs.infoModal.close()">
<i>keyboard_arrow_left</i>
</button>
<q-toolbar-title :padding="1">
사용자 상세보기
</q-toolbar-title>
</div>
<div class="layout-view">
<div class="layout-padding">
<table class="q-table bordered flipped">
<thead>
<tr>
<th>이름</th>
<th>성명</th>
<th>상태</th>
<th>설명</th>
<th>만료일</th>
<th>최종 수정일</th>
<th>생성일</th>
</tr>
</thead>
<tbody>
<tr>
<td>&nbsp; {{ user.name }} </td>
<td>&nbsp; {{ user.cn }} </td>
<td>&nbsp; {{ user.state }} </td>
<td>&nbsp; {{ user.desc }} </td>
<td>&nbsp;
{{ user.expireAt ? new Date(user.expireAt).toLocaleString() : '' }}
</td>
<td>&nbsp;
{{ user.modifiedAt ? new Date(user.modifiedAt).toLocaleString() : '' }}
</td>
<td>&nbsp;
{{ user.createdAt ? new Date(user.createdAt).toLocaleString() : '' }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</q-layout>
</q-modal>
</div>
</template>
<script>
import { API_URL } from 'config'
import { Dialog, Toast } from 'quasar'
export default {
name: 'userList',
data: function () {
return {
intervalObj: [],
intervalRefresh: [],
data: [],
user: {},
config: {
rowHeight: '10px',
title: '사용자 목록',
refresh: false,
columnPicker: true,
pagination: {
rowsPerPage: 10,
options: [5, 10, 15, 30, 50, 100]
},
messages: {
noData: '<i>warning</i> 데이터가 없습니다.',
noDataAfterFiltering: '<i>warning</i> 검색결과가 없습니다.'
}
},
columns: [
{
label: 'ID',
field: 'name',
width: '80px',
filter: true,
sort: true
},
{
label: '성명',
field: 'cn',
width: '80px',
filter: true,
sort: true
},
{
label: '상태',
field: 'state',
width: '100px',
filter: true,
sort: true
},
{
label: '만료일',
field: 'expireAt',
width: '150px',
filter: 'date',
sort: true,
format (value) {
return value ? new Date(value).toLocaleString() : ''
}
},
{
label: '수정일',
field: 'modifiedAt',
width: '150px',
filter: 'date',
sort: true,
format (value) {
return value ? new Date(value).toLocaleString() : ''
}
},
{
label: '생성일',
field: 'createdAt',
width: '150px',
filter: 'date',
sort: true,
format (value) { return new Date(value).toLocaleString() }
},
{ label: '기능', field: 'functions', width: '150px', sort: false }
]
}
},
methods: {
goToCreate: function () {
this.$router.push('/account/create')
},
goToEdit: function (name) {
this.$router.push('/account/' + name)
},
goToPassword: function (name) {
this.$router.push('/account/' + name + '/pass')
},
confirmDelete: function (name) {
let self = this
Dialog.create({
title: '확인',
message: `사용자 ${name} 삭제하시겠습니까?`,
buttons: [
{
label: '아니오'
},
{
label: '예',
handler () {
self.processDelete(name)
self.loadData()
}
}
]
})
},
processDelete: function (name) {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + `/account/${name}`
this.$http.delete(url, headers).then(response => {
Toast.create.positive({
html: `사용자 ${name} 삭제 성공`,
timeout: 5000
})
}, response => {
let err = `사용자 ${name} 삭제 실패`
Toast.create.negative({
html: err,
timeout: 5000
})
})
},
getUserInfo: function (name) {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + `/account/${name}`
this.$http.get(url, headers).then(response => {
this.user = response.body
this.$refs.infoModal.open()
}, response => {
Toast.create.negative('계정 정보 수신 실패')
})
},
loadData: function () {
const url = API_URL + '/account'
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
this.$http.get(url, headers).then(response => {
this.data = response.body
}, response => {
Toast.create.negative('계정 정보 수신 실패')
})
},
refresh: function () {
const refreshToken = this.$store.state.refreshToken
const headers = {
headers: {
'Authorization': `Bearer ${refreshToken}`
}
}
const url = API_URL + '/account/refresh'
this.$http.post(url, {}, headers).then(response => {
this.$store.commit('setToken', response.body.token)
}, response => {
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
this.loadData()
this.intervalObj = setInterval(this.loadData, 10 * 1000)
this.refresh()
this.intervalRefresh = setInterval(this.refresh, 5 * 60 * 1000)
},
destroyed: function () {
clearInterval(this.intervalObj)
clearInterval(this.intervalRefresh)
}
}
</script>
<style>
</style>
<template>
<div class="column">
<h6>
사용자 비밀번호 변경
</h6>
<p></p>
<form>
<div class="stacked-label">
<label>아이디</label>
<input
type="text"
v-model="form.name"
ref="name"
readonly
>
</div>
<p></p>
<div class="stacked-label">
<label>새 비밀번호 (알파벳, 숫자, 특수문자 조합. 8자 이상)</label>
<input
type="password"
size="50"
v-model="form.pass"
ref="pass"
placeholder="새 비밀번호를 입력하세요."
:class="{'has-error': $v.form.pass.$error}"
>
</div>
<span
class="text-negative"
v-if="!$v.form.pass.required"
> 새 비밀번호는 필수 입력 항목입니다.
</span>
<span
class="text-negative"
v-if="!$v.form.pass.minLength"
> 비밀번호는 8자 이상이어야 합니다.
</span>
<p></p>
<div class="stacked-label">
<label>새 비밀번호 재입력</label>
<input
type="password"
size="50"
v-model="form.pass2"
ref="pass2"
placeholder="새 비밀번호를 재입력하세요."
:class="{'has-error': $v.form.pass2.$error}"
>
</div>
<span
class="text-negative"
v-if="!$v.form.pass2.sameAsPass"
> 비밀번호가 일치하지 않습니다.
</span>
<p></p>
<div>
<button class="purple" @click="submit">
비밀번호 변경
</button>
</div>
</form>
</div>
</template>
<script>
import { API_URL, checkStrength } from 'config'
import { Toast } from 'quasar'
import { required, minLength, sameAs } from 'vuelidate/lib/validators'
export default {
name: 'userCreate',
data () {
return {
form: {
name: this.$route.params.name,
pass: '',
pass2: ''
}
}
},
validations: {
form: {
pass: {
required,
minLength: minLength(8)
},
pass2: {
required,
sameAsPass: sameAs('pass')
}
}
},
methods: {
submit: function () {
if (checkStrength(this.form.pass) < 3) {
Toast.create.negative('비밀번호가 보안규약에 위배됩니다.')
return
}
if (this.form.pass !== this.form.pass2) {
Toast.create.negative('비밀번호가 일치하지 않습니다.')
return
}
const payload = {
name: this.form.name,
pass: this.form.pass,
pass2: this.form.pass2
}
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/account' + '/' + this.form.name + '/pass'
this.$http.patch(url, payload, headers).then(response => {
this.$router.push('/account')
}, response => {
Toast.create.negative('사용자 수정 실패: ' + response.body['error'])
})
},
loadData: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
let url = API_URL + '/account' + '/' + this.$route.params.name
this.$http.get(url, headers).then(response => {
let rep = response.body
this.form.cn = rep.cn ? rep.cn : ''
this.form.state = rep.state ? rep.state : ''
this.form.expireAt = rep.expireAt ? rep.expireAt : ''
this.form.desc = rep.desc ? rep.desc : ''
}, response => {
Toast.create.negative('사용자 정보 수신 실패')
})
}
},
mounted: function () {
this.loadData()
}
}
</script>
<style>
</style>
<template>
<div>
<h6>
애플리케이션 생성
</h6>
<p></p>
<form>
<div class="stacked-label">
<input
type="text"
v-model="form.name"
ref="name"
class="full-width"
:class="{'has-error': $v.form.name.$error}"
placeholder="애플리케이션 이름을 입력하세요."
>
<label>이름</label>
<span
class="text-negative"
v-if="!$v.form.name.required">
이름은 필수 입력 항목입니다.
</span>
<span
class="text-negative"
v-if="!$v.form.name.minLength">
이름은 3문자 이상이어야 합니다.
</span>
<span
class="text-negative"
v-if="!$v.form.name.alphaNum">
이름은 알파벳과 숫자 조합입니다.
</span>
</div>
<p></p>
<div class="stacked-label">
<label>레포지토리 이름</label><p></p>
<input
type="text"
v-model="form.repo"
ref="repo"
class="full-width"
:class="{'has-error': $v.form.repo.$error}"
placeholder="레포지토리 이름을 입력하세요."
>
<span
class="text-negative"
v-if="!$v.form.repo.required">
레포지토리 이름은 필수 입력 항목입니다.
</span>
</div>
<p></p>
<div class="stacked-label">
<label>설명</label><p></p>
<input
type="text"
v-model="form.desc"
ref="desc"
class="full-width"
placeholder="설명을 입력하세요."
>
</div>
<p></p>
<div>
<button class="purple" @click="submit">
생성
</button>
</div>
</form>
</div>
</template>
<script>
import { API_URL } from 'config'
import { Toast } from 'quasar'
import { required, minLength, alphaNum } from 'vuelidate/lib/validators'
export default {
name: 'appCreate',
data () {
return {
form: {
name: '',
repo: '',
desc: ''
}
}
},
validations: {
form: {
name: {
required,
alphaNum,
minLength: minLength(3)
},
repo: {
required
}
}
},
methods: {
submit: function () {
const payload = {
name: this.form.name,
repo: this.form.repo,
desc: this.form.desc
}
if (!payload.repo) {
console.log(payload)
return
}
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/app'
this.$http.post(url, payload, headers).then(response => {
this.$router.push('/app')
}, response => {
console.log(response.status)
Toast.create.negative('애플리케이션 정보 수신 실패')
this.$refs.name.focus()
})
}
},
mounted () {
if (!this.$store.state.login) { this.$router.push('/') }
this.$refs.name.focus()
}
}
</script>
<style>
</style>
<template>
<div>
<h6>
서버 생성
</h6>
<p></p>
<form>
<div class="stacked-label">
<input
type="text"
v-model="form.name"
ref="name"
class="full-width"
:class="{'has-error': $v.form.name.$error}"
placeholder="Type machine name"
>
<label>이름</label>
<span
class="text-negative"
v-if="!$v.form.name.required">
Field is required
</span>
<span
class="text-negative"
v-if="!$v.form.name.minLength">
Name must be longer than 3 letters.
</span>
<span
class="text-negative"
v-if="!$v.form.name.alphaNum">
Name must be alphanumeric or hyphen.
</span>
</div>
<p></p>
<div class="stacked-label">
<label>종류</label><p></p>
<q-select
type="radio"
v-model="form.type"
ref="type"
:options="machineType">
</q-select>
</div>
<p></p>
<div class="stacked-label">
<label>OS</label><p></p>
<q-select
type="radio"
v-model="form.os"
ref="os"
:options="selectOS">
</q-select>
</div>
<p></p>
<div class="stacked-label">
<label>MAC주소</label><p></p>
<input
type="text"
v-model="form.mac"
ref="mac"
class="full-width"
:class="{'has-error': $v.form.mac.$error}"
placeholder="Type machine Mac Address"
>
<span
class="text-negative"
v-if="!$v.form.mac.required">
Field is required
</span>
</div>
<p></p>
<div class="stacked-label">
<label>IP주소</label><p></p>
<input
type="text"
v-model="form.ip"
ref="ip"
class="full-width"
:class="{'has-error': $v.form.ip.$error}"
placeholder="Type machine IP Address"
>
<span
class="text-negative"
v-if="!$v.form.ip.required">
Field is required
</span>
</div>
<p></p>
<div class="stacked-label">
<label>설명</label><p></p>
<input
type="text"
v-model="form.desc"
ref="desc"
class="full-width"
placeholder="Type machine description"
>
</div>
<p></p>
<div>
<button class="purple" @click="submit">
생성
</button>
</div>
</form>
</div>
</template>
<script>
import { API_URL, selectOS } from 'config'
import { Toast } from 'quasar'
import { required, minLength, alphaNum } from 'vuelidate/lib/validators'
export default {
name: 'machineCreate',
data () {
return {
form: {
name: '',
type: '',
os: '',
mac: '',
ip: '',
desc: ''
},
machineType: [
{'label': 'PM', value: 'PM'},
{'label': 'VM', value: 'VM'}
],
selectOS: selectOS
}
},
validations: {
form: {
name: {
required,
alphaNum,
minLength: minLength(3)
},
mac: {
required
},
ip: {
required
}
}
},
methods: {
submit: function () {
const url = API_URL + '/machine'
const payload = {
name: this.form.name,
type: this.form.type,
os: this.form.os,
mac: this.form.mac,
ip: this.form.ip,
desc: this.form.desc,
preseed: ''
}
if (!payload.os) {
console.log(payload)
return
}
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
this.$http.post(url, payload, headers).then(response => {
this.$router.push('/machine')
}, response => {
Toast.create.negative(response.statusText)
this.$refs.name.focus()
})
}
},
mounted () {
if (!this.$store.state.login) { this.$router.push('/') }
this.$refs.name.focus()
}
}
</script>
<style>
</style>
<template>
<div class="column">
<h6>
그룹 생성
</h6>
<p></p>
<form>
<div class="stacked-label">
<input
type="text"
v-model="form.name"
ref="name"
class="full-width"
:class="{'has-error': $v.form.name.$error}"
placeholder="그룹 고유명을 입력하세요."
>
<label>고유명</label>
<span
class="text-negative"
v-if="!$v.form.name.required">
고유명은 필수 입력 항목입니다.
</span>
<span
class="text-negative"
v-if="!$v.form.name.minLength">
고유명은 3문자 이상이어야 합니다.
</span>
<span
class="text-negative"
v-if="!$v.form.name.alphaNum">
고유명은 알파벳과 숫자의 조합이어야 합니다.
</span>
</div>
<div class="stacked-label">
<input
type="text"
size="50"
v-model="form.cn"
ref="cn"
class="full-width"
:class="{'has-error': $v.form.cn.$error}"
placeholder="그룹 일반명을 입력하세요."
>
<label>일반명</label>
<span
class="text-negative"
v-if="!$v.form.cn.required">
일반명은 필수 입력 항목입니다.
</span>
<span v-if="!$v.form.cn.minLength">
일반명은 3글자 이상이어야 합니다.
</span>
</div>
<div class="stacked-label">
<input
type="text"
size="50"
v-model="form.desc"
ref="desc"
class="full-width"
placeholder="그룹 설명을 입력하세요."
>
<label>설명</label>
</div>
<div>
<button class="purple" @click="submit">
생성
</button>
</div>
</div>
</template>
<script>
import { API_URL } from 'config'
import { Toast } from 'quasar'
import { required, minLength, alphaNum } from 'vuelidate/lib/validators'
export default {
name: 'sectionCreate',
data () {
return {
form: {
name: '',
cn: '',
desc: ''
}
}
},
validations: {
form: {
name: {
required,
alphaNum,
minLength: minLength(3)
},
cn: {
required,
minLength: minLength(3)
}
}
},
methods: {
submit: function () {
const url = API_URL + '/section'
const payload = {
name: this.form.name,
cn: this.form.cn,
desc: this.form.desc
}
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
this.$http.post(url, payload, headers).then(response => {
console.log(response.status)
this.$router.push('/section')
}, response => {
Toast.create.negative('그룹 생성 실패')
this.$refs.name.focus()
})
}
},
mounted () {
if (!this.$store.state.login) { this.$router.push('/') }
this.$refs.name.focus()
}
}
</script>
<style>
</style>
<template>
<div>
<button
class="purple small"
@click="goToCreate"
>
<i>create_new_folder</i> 생성
</button>
<q-data-table
:data="sections"
:config="config"
:columns="columns"
>
<template
slot="col-functions"
scope="cell"
>
<button
class="purple small circular"
@click="processEdit(cell.row)"
>
<i>edit</i>
</button>
<button
class="purple small circular"
@click="confirmDelete(cell.row.name)"
v-bind:disabled="!isAdmin()"
>
<i>delete</i>
</button>
</template>
</q-data-table>
</div>
</div>
</template>
<script>
import { API_URL } from 'config'
import { Dialog, Toast } from 'quasar'
export default {
name: 'sectionList',
data: function () {
return {
role: this.$store.state.role,
intervalObj: [],
intervalRefresh: [],
sections: [],
config: {
title: '그룹 목록',
refresh: false,
columnPicker: true,
rowHeight: '10px',
pagination: {
rowsPerPage: 10,
options: [5, 10, 15, 30, 50, 100]
},
messages: {
noData: '<i>warning</i> 데이터가 없습니다.',
noDataAfterFiltering: '<i>warning</i> 검색 결과가 없습니다.'
}
},
columns: [
{
label: '고유명',
field: 'name',
width: '100px',
filter: true,
sort: true
},
{
label: '일반명',
field: 'cn',
width: '100px',
filter: true,
sort: true
},
{
label: '설명',
field: 'desc',
width: '100px',
filter: true,
sort: false
},
{
label: '생성일',
field: 'createdAt',
width: '100px',
filter: true,
sort: 'date',
format (value) {
return new Date(value).toLocaleString()
}
},
{ label: '기능', field: 'functions', width: '100px', sort: false }
]
}
},
methods: {
goToCreate: function () {
this.$router.push('/section/create')
},
processEdit: function (row) {
let self = this
Dialog.create({
title: `그룹: ${row.name}`,
message: '수정',
form: {
cn: {
type: 'textbox',
label: '일반명',
model: row.cn
},
desc: {
type: 'textbox',
label: '설명',
model: row.desc
}
},
buttons: [
'취소',
{
label: '확인',
preventClose: true,
handler (data, close) {
// Validate data
if (data.cn.length < 4) {
let err = '일반명은 4글자 이상이어야 합니다.'
Toast.create.negative(err)
return
}
else { // all validated. submit it.
const url = API_URL + `/section/${row.name}`
const token = self.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
self.$http.patch(url, data, headers).then(response => {
// close dialog
close(() => {
Toast.create.positive({
html: `그룹 ${row.name} 수정 성공`,
timeout: 5000
})
})
self.loadData()
}, response => {
let err = `그룹 ${row.name} 수정 실패`
Toast.create.negative({
html: err,
timeout: 5000
})
})
}
}
}
]
})
},
confirmDelete: function (name) {
let self = this
Dialog.create({
title: 'Confirm',
message: `그룹 ${name} 삭제하시겠습니까?`,
buttons: [
'아니오',
{
label: '예',
handler () {
self.processDelete(name)
self.loadData()
}
}
]
})
},
processDelete: function (name) {
const url = API_URL + `/section/${name}`
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
this.$http.delete(url, headers).then(response => {
Toast.create.positive({
html: `The section ${name} is deleted.`,
timeout: 5000
})
}, response => {
let err = `Deleting section ${name} is failed. ` + response.statusText
Toast.create.negative({
html: err,
timeout: 5000
})
})
},
isAdmin: function () {
return this.$store.state.role === 'admin'
},
loadData: function () {
const token = this.$store.state.token
const headers = {
headers: {
'Authorization': `Bearer ${token}`
}
}
const url = API_URL + '/section'
this.$http.get(url, headers).then(response => {
this.sections = response.body
}, response => {
if (response.status === 401) {
this.$router.push('/login')
}
})
},
refresh: function () {
const refreshToken = this.$store.state.refreshToken
const headers = {
headers: {
'Authorization': `Bearer ${refreshToken}`
}
}
const url = API_URL + '/account/refresh'
this.$http.post(url, {}, headers).then(response => {
this.$store.commit('setToken', response.body.token)
}, response => {
})
}
},
mounted: function () {
if (!this.$store.state.login) { this.$router.push('/') }
this.loadData()
this.intervalObj = setInterval(this.loadData, 10 * 1000)
this.refresh()
this.intervalRefresh = setInterval(this.refresh, 5 * 60 * 1000)
},
destroyed: function () {
clearInterval(this.intervalObj)
clearInterval(this.intervalRefresh)
}
}
</script>
<style>
</style>
export const API_URL = 'http://121.254.203.198:8000/api'
export const selectOS = [
{label: 'CENTOS7', value: 'CENTOS7'},
{label: 'UXENOS', value: 'UXENOS'},
{label: 'JESSIE', value: 'JESSIE'}
]
export function checkStrength (pw) {
// initialize
var iStrength = 0
// check length
if (pw.length < 8) {
return iStrength
}
if (pw.length > 7) iStrength += 1
if (pw.length > 9) iStrength += 1
if (pw.match(/[a-zA-Z]/) && pw.match(/[0-9]/)) iStrength += 1
if (pw.match(/\W/)) iStrength += 1
return iStrength
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
<title>UDAM</title>
<link rel="shortcut icon" type="image/x-icon" href="statics/favicon.ico">
</head>
<body>
<div id="q-app"></div>
<!-- built files will be auto injected -->
</body>
</html>
// === DEFAULT / CUSTOM STYLE ===
// WARNING! always comment out ONE of the two require() calls below.
// 1. use next line to activate CUSTOM STYLE (./src/themes)
// require(`./themes/app.${__THEME}.styl`)
// 2. or, use next line to activate DEFAULT QUASAR STYLE
require(`quasar/dist/quasar.${__THEME}.css`)
// ==============================
import Vue from 'vue'
import Quasar from 'quasar'
import router from './router'
import VueResource from 'vue-resource'
import VueCookie from 'vue-cookie'
import store from './store'
import Vuelidate from 'vuelidate'
Vue.use(Quasar) // Install Quasar Framework
Vue.use(VueResource)
Vue.use(VueCookie)
Vue.use(Vuelidate)
Quasar.start(() => {
/* eslint-disable no-new */
new Vue({
el: '#q-app',
router,
store,
render: h => h(require('./App'))
})
})
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
function load (component) {
return () => System.import(`components/${component}.vue`)
}
export default new VueRouter({
/*
* NOTE! VueRouter "history" mode DOESN'T works for Cordova builds,
* it is only to be used only for websites.
*
* If you decide to go with "history" mode, please also open /config/index.js
* and set "build.publicPath" to something other than an empty string.
* Example: '/' instead of current ''
*
* If switching back to default "hash" mode, don't forget to set the
* build publicPath back to '' so Cordova builds work again.
*/
routes: [
{
path: '/login',
name: 'login',
component: load('Login')
},
{
path: '/logout',
name: 'logout',
component: load('Logout')
},
{
path: '/section',
name: 'section',
component: load('section/List')
},
{
path: '/section/create',
name: 'sectionCreate',
component: load('section/Create')
},
{
path: '/machine',
name: 'machine',
component: load('machine/List')
},
{
path: '/machine/create',
name: 'machineCreate',
component: load('machine/Create')
},
{
path: '/app',
name: 'app',
component: load('app/List')
},
{
path: '/app/create',
name: 'appCreate',
component: load('app/Create')
},
{
path: '/account',
name: 'account',
component: load('account/List')
},
{
path: '/account/create',
name: 'accountCreate',
component: load('account/Create')
},
{
path: '/account/:name',
name: 'accountEdit',
component: load('account/Edit')
},
{
path: '/account/:name/pass',
name: 'accountPassword',
component: load('account/Pass')
},
{
path: '/',
name: 'index',
component: load('Index')
},
{
path: '*',
component: load('Error404')
}
]
})
import Vue from 'vue'
import Vuex from 'vuex'
import VueCookie from 'vue-cookie'
Vue.use(Vuex)
Vue.use(VueCookie)
export default new Vuex.Store({
state: {
login: false,
role: '',
token: '',
refreshToken: '',
name: ''
},
mutations: {
loggedIn (state, bLogin) {
state.login = bLogin
},
setName (state, sName) {
state.name = sName
},
setRole (state, sRole) {
state.role = sRole
},
setToken (state, sToken) {
state.token = sToken
},
setRefreshToken (state, sRefreshToken) {
state.refreshToken = sRefreshToken
}
}
})
// This file is included in the build if src/main.js imports it.
// Otherwise the default iOS CSS file is bundled.
// Check "DEFAULT / CUSTOM STYLE" in src/main.js
// App Shared Variables
// --------------------------------------------------
// Shared Stylus variables go in the app.variables.styl file
@import 'app.variables'
// App iOS Design Variables
// --------------------------------------------------
// iOS Design only Stylus variables can go here
$generic-border-radius ?= 4px
$generic-input-size ?= 30px
$generic-input-border-width ?= 2px
$typography-font-family ?= '-apple-system', 'Helvetica Neue', Helvetica, Arial, sans-serif
$toolbar-color ?= #424242
$toolbar-background ?= $light
$toolbar-active-color ?= $light
$toolbar-faded-color ?= composite-color($toolbar-color)
// Quasar iOS Design Stylus
// --------------------------------------------------
// Custom App variables must be declared before importing Quasar.
// Quasar will use its default values when a custom variable isn't provided.
@import '~quasar-framework/dist/quasar.ios.styl'
// This file is included in the build if src/main.js imports it.
// Otherwise the default Material CSS file is bundled.
// Check "DEFAULT / CUSTOM STYLE" in src/main.js
// App Shared Variables
// --------------------------------------------------
// Shared Stylus variables go in the app.variables.styl file
@import 'app.variables'
// App Material Design Variables
// --------------------------------------------------
// Material Design only Stylus variables can go here
$generic-border-radius ?= 2px
$generic-input-size ?= 25px
$generic-input-border-width ?= 2px
$typography-font-family ?= 'Roboto'
$toolbar-color ?= white
$toolbar-background ?= $primary
$toolbar-active-color ?= $primary
$toolbar-faded-color ?= composite-color($primary)
// Quasar Material Design Stylus
// --------------------------------------------------
// Custom App variables must be declared before importing Quasar.
// Quasar will use its default values when a custom variable isn't provided.
@import '~quasar-framework/dist/quasar.mat.styl'
// This file is included in the build if src/main.js imports
// either app.mat.styl or app.ios.styl.
// Check "DEFAULT / CUSTOM STYLE" in src/main.js
// App Shared Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the Stylus variables found in Quasar's source Stylus files. Setting
// variables before Quasar's Stylus will use these variables rather than
// Quasar's default Stylus variable values. Stylus variables specific
// to the themes belong in either the app.ios.styl or app.mat.styl files.
// App Shared Color Variables
// --------------------------------------------------
// It's highly recommended to change the default colors
// to match your app's branding.
$primary = #027be3
$secondary = #26A69A
$tertiary = #555
$neutral = #E0E1E2
$positive = #21BA45
$negative = #DB2828
$info = #31CCEC
$warning = #F2C037
$light = #f4f4f4
$dark = #333
$faded = #777
$text-color = lighten(black, 17%)
$background-color = white
$link-color = lighten($primary, 25%)
$link-color-active = $primary
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment