Implement OAuth2 server, link oauth:Client and auth::Oauth2Client, implement permission checks
This commit is contained in:
@@ -5,52 +5,111 @@ import { resource_service } from '../service/Resource.service.js'
|
||||
import { action_service } from '../service/Action.service.js'
|
||||
|
||||
const template = `
|
||||
<div class="card col-12 col-sm-10 offset-sm-1 col-md-8 offset-md-2 col-xl-6 offset-xl-3">
|
||||
<h3 class="card-title mb-4 mt-4">
|
||||
<button class="btn-lg btn" @click="back"><i class="fa fa-arrow-left"></i></button> {{ title }}
|
||||
</h3>
|
||||
<form v-on:submit.prevent="do_nothing" v-if="is_ready">
|
||||
<div class="form-group" v-for="field of definition.fields">
|
||||
<span v-if="field.type.startsWith('select')">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<select
|
||||
:id="uuid+field.field"
|
||||
class="form-control"
|
||||
v-model="data[field.field]"
|
||||
:required="field.required"
|
||||
:readonly="mode === 'view'"
|
||||
:multiple="!!field.type.endsWith('.multiple')"
|
||||
>
|
||||
<option v-for="option of field.options" :value="option.value">{{ typeof option.display === 'function' ? option.display(option) : option.display }}</option>
|
||||
</select>
|
||||
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||
</span>
|
||||
<span v-if="field.type === 'text'">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
:id="uuid+field.field"
|
||||
v-model="data[field.field]"
|
||||
:required="field.required"
|
||||
:placeholder="field.placeholder"
|
||||
:readonly="mode === 'view'"
|
||||
ref="input"
|
||||
>
|
||||
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||
</span>
|
||||
<div class="card col-12 col-sm-10 offset-sm-1 col-md-8 offset-md-2 col-xl-6 offset-xl-3 mb-5">
|
||||
<span v-if="!can_access">
|
||||
<div class="row m-5">
|
||||
<div class="col-12 text-center">
|
||||
<h5>{{ access_msg }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-12 text-right mb-4 mr-0 mt-2">
|
||||
<span style="color: darkred;" class="font-italic mr-3" v-if="error_message">{{ error_message }}</span>
|
||||
<span class="font-italic mr-3 text-muted" v-if="other_message">{{ other_message }}</span>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
v-if="mode !== 'view'"
|
||||
@click="save_click"
|
||||
>Save</button>
|
||||
</div>
|
||||
</span>
|
||||
<span v-if="can_access">
|
||||
<h3 class="card-title mb-4 mt-4">
|
||||
<button class="btn-lg btn" @click="back"><i class="fa fa-arrow-left"></i></button> {{ title }}
|
||||
</h3>
|
||||
<div class="row" v-if="!is_ready">
|
||||
<div class="col-12 text-center pad-top mb-5">
|
||||
<h4>Loading...</h4>
|
||||
</div>
|
||||
</div>
|
||||
<form v-on:submit.prevent="do_nothing" v-if="is_ready">
|
||||
<div class="form-group" v-for="field of definition.fields">
|
||||
<span
|
||||
v-if="field.type === 'display' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))"
|
||||
v-html="typeof field.display === 'function' ? field.display(data) : field.display"
|
||||
></span>
|
||||
<span v-if="field.type.startsWith('select') && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<select
|
||||
:id="uuid+field.field"
|
||||
class="form-control"
|
||||
v-model="data[field.field]"
|
||||
:required="Array.isArray(field.required) ? field.required.includes(mode) : field.required"
|
||||
:readonly="mode === 'view' || (Array.isArray(field.readonly) ? field.readonly.includes(mode) : field.readonly)"
|
||||
:multiple="!!field.type.endsWith('.multiple')"
|
||||
ref="input"
|
||||
>
|
||||
<option
|
||||
v-for="option of field.options"
|
||||
:value="option.value"
|
||||
:selected="data[field.field] && data[field.field].includes(option.value)"
|
||||
>{{ typeof option.display === 'function' ? option.display(option) : option.display }}</option>
|
||||
</select>
|
||||
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||
</span>
|
||||
<span v-if="field.type === 'text' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
:id="uuid+field.field"
|
||||
v-model="data[field.field]"
|
||||
:required="Array.isArray(field.required) ? field.required.includes(mode) : field.required"
|
||||
:placeholder="field.placeholder"
|
||||
:readonly="mode === 'view' || (Array.isArray(field.readonly) ? field.readonly.includes(mode) : field.readonly)"
|
||||
ref="input"
|
||||
>
|
||||
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||
</span>
|
||||
<span v-if="field.type === 'textarea' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
:id="uuid+field.field"
|
||||
v-model="data[field.field]"
|
||||
:required="Array.isArray(field.required) ? field.required.includes(mode) : field.required"
|
||||
:placeholder="field.placeholder"
|
||||
:readonly="mode === 'view' || (Array.isArray(field.readonly) ? field.readonly.includes(mode) : field.readonly)"
|
||||
ref="input"
|
||||
></textarea>
|
||||
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||
</span>
|
||||
<span v-if="field.type === 'password' && (Array.isArray(field.hidden) ? !field.hidden.includes(mode) : !field.hidden) && (typeof field.if !== 'function' || field.if(data))">
|
||||
<label :for="uuid+field.field">{{ field.name }}</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
:id="uuid+field.field"
|
||||
v-model="data[field.field]"
|
||||
:required="Array.isArray(field.required) ? field.required.includes(mode) : field.required"
|
||||
:placeholder="field.placeholder"
|
||||
:readonly="mode === 'view' || (Array.isArray(field.readonly) ? field.readonly.includes(mode) : field.readonly)"
|
||||
ref="input"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
:id="uuid+field.field+'-confirm'"
|
||||
v-model="data[field.field+'-confirm']"
|
||||
:required="Array.isArray(field.required) ? field.required.includes(mode) : field.required"
|
||||
:placeholder="'Confirm ' + field.name"
|
||||
:readonly="mode === 'view' || (Array.isArray(field.readonly) ? field.readonly.includes(mode) : field.readonly)"
|
||||
>
|
||||
<small class="form-text" style="color: darkred;" v-if="field.error">{{ field.error }}</small>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-12 text-right mb-4 mr-0 mt-2">
|
||||
<span style="color: darkred;" class="font-italic mr-3" v-if="error_message">{{ error_message }}</span>
|
||||
<span class="font-italic mr-3 text-muted" v-if="other_message">{{ other_message }}</span>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
v-if="mode !== 'view'"
|
||||
@click="save_click"
|
||||
>Save</button>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -80,6 +139,9 @@ export default class FormComponent extends Component {
|
||||
error_message = ''
|
||||
other_message = ''
|
||||
|
||||
access_msg = ''
|
||||
can_access = false
|
||||
|
||||
is_ready = false
|
||||
mode = ''
|
||||
id = ''
|
||||
@@ -99,49 +161,62 @@ export default class FormComponent extends Component {
|
||||
this.mode = this.initial_mode
|
||||
this.id = this.form_id
|
||||
this.resource_class = await resource_service.get(this.resource)
|
||||
|
||||
if ( await this.resource_class.can(this.mode) ) {
|
||||
this.can_access = true
|
||||
this.access_msg = true
|
||||
} else {
|
||||
this.can_access = false
|
||||
this.access_msg = 'Sorry, you do not have permission to ' + this.mode + ' this resource.'
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
this.reset()
|
||||
}
|
||||
|
||||
this.uuid = utility.uuid()
|
||||
await this.load()
|
||||
await this.init()
|
||||
await this.load()
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.definition = this.resource_class.form_definition
|
||||
for ( const field of this.definition.fields ) {
|
||||
if ( field.type.startsWith('select.dynamic') ) {
|
||||
field._options = field._options || field.options
|
||||
const rsc = await resource_service.get(field._options.resource)
|
||||
const other_params = field._options.other_params || {}
|
||||
|
||||
field.options = (await rsc.list()).map(item => {
|
||||
field.options = (await rsc.list(other_params)).map(item => {
|
||||
return {
|
||||
display: typeof field._options.display === 'function' ? field._options.display(item) : item[field._options.display || 'display'],
|
||||
value: typeof field._options.value === 'function' ? field._options.value(item) : item[field._options.value || 'display'],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( field.type.endsWith('.multiple') ) {
|
||||
async load() {
|
||||
if (this.mode !== 'insert') {
|
||||
this.data = await this.resource_class.get(this.id)
|
||||
}
|
||||
|
||||
for ( const field of this.definition.fields ) {
|
||||
if ( field.type.endsWith('.multiple') && !this.data[field.field] ) {
|
||||
this.data[field.field] = []
|
||||
}
|
||||
}
|
||||
|
||||
this.title = title_map[this.mode] + ' ' + this.resource_class.item
|
||||
|
||||
this.is_ready = true
|
||||
this.$nextTick(() => {
|
||||
if ( this.mode !== 'view' ) this.$refs.input[0].focus()
|
||||
})
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.definition = this.resource_class.form_definition
|
||||
if (this.mode !== 'insert') {
|
||||
this.data = await this.resource_class.get(this.id)
|
||||
}
|
||||
|
||||
this.title = title_map[this.mode] + ' ' + this.resource_class.item
|
||||
}
|
||||
|
||||
async on_create() {
|
||||
this.id = this.data.id
|
||||
this.mode = 'update'
|
||||
@@ -171,9 +246,12 @@ export default class FormComponent extends Component {
|
||||
validate() {
|
||||
let valid = true
|
||||
for ( const field of this.definition.fields ) {
|
||||
if ( field.required && (!(field.field in this.data) || !this.data[field.field]) ) {
|
||||
if ( (Array.isArray(field.required) ? field.required.includes(this.mode) : field.required) && (!(field.field in this.data) || !this.data[field.field]) ) {
|
||||
field.error = 'This field is required.'
|
||||
valid = false
|
||||
} else if ( field.type === 'password' && this.data[field.field] !== this.data[field.field + '-confirm'] ) {
|
||||
field.error = field.name + ' confirmation does not match.'
|
||||
valid = false
|
||||
} else {
|
||||
field.error = ''
|
||||
}
|
||||
|
||||
@@ -1,49 +1,62 @@
|
||||
import { Component } from '../../lib/vues6/vues6.js'
|
||||
import { action_service } from '../service/Action.service.js'
|
||||
import { message_service } from '../service/Message.service.js'
|
||||
import { resource_service } from '../service/Resource.service.js'
|
||||
|
||||
const template = `
|
||||
<div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-10"><h3>{{ resource_class.plural }}</h3></div>
|
||||
<div class="col-2 text-right" v-if="definition.actions">
|
||||
<button
|
||||
:class="['btn', 'btn-'+(action.color || 'secondary'), 'btn-sm']"
|
||||
type="button"
|
||||
v-for="action of definition.actions"
|
||||
@click="perform($event, action)"
|
||||
v-if="action.position === 'main'"
|
||||
>{{ action.text }}</button>
|
||||
<span v-if="!can_access">
|
||||
<div class="row m-5">
|
||||
<div class="col-12 text-center">
|
||||
<h4 class="pad-top">{{ access_msg }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col" v-for="col of definition.columns">{{ col.name }}</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) of data">
|
||||
<th scope="row">{{ index + 1 }}</th>
|
||||
<td v-for="col of definition.columns">
|
||||
<span v-if="typeof col.renderer === 'function'">{{ col.renderer(row[col.field]) }}</span>
|
||||
<span v-if="col.renderer === 'boolean'">{{ row[col.field] ? 'Yes' : 'No' }}</span>
|
||||
<span v-if="col.renderer !== 'boolean' && typeof col.renderer !== 'function'">{{ col.field in row ? row[col.field] : '-' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
:class="['mr-2', 'btn', 'btn-sm', 'btn-'+(action.color || 'secondary')]"
|
||||
v-for="action of definition.actions"
|
||||
v-if="action.position === 'row'"
|
||||
@click="perform($event, action, row)"
|
||||
><i :class="action.icon" v-if="action.icon"></i> {{ action.text }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
<span v-if="can_access">
|
||||
<div class="row mb-4">
|
||||
<div class="col-10"><h3>{{ resource_class.plural }}</h3></div>
|
||||
<div class="col-2 text-right" v-if="definition.actions">
|
||||
<button
|
||||
:class="['btn', 'btn-'+(action.color || 'secondary'), 'btn-sm']"
|
||||
type="button"
|
||||
v-for="action of definition.actions"
|
||||
@click="perform($event, action)"
|
||||
v-if="action.position === 'main'"
|
||||
>{{ action.text }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4" v-if="definition.display">
|
||||
<div class="col-12" v-html="definition.display"></div>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col" v-for="col of definition.columns">{{ col.name }}</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) of data">
|
||||
<th scope="row">{{ index + 1 }}</th>
|
||||
<td v-for="col of definition.columns">
|
||||
<span v-if="typeof col.renderer === 'function'">{{ col.renderer(row[col.field]) }}</span>
|
||||
<span v-if="col.renderer === 'boolean'">{{ row[col.field] ? 'Yes' : 'No' }}</span>
|
||||
<span v-if="col.renderer !== 'boolean' && typeof col.renderer !== 'function'">{{ col.field in row ? row[col.field] : '-' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
:class="['mr-2', 'btn', 'btn-sm', 'btn-'+(action.color || 'secondary')]"
|
||||
v-for="action of definition.actions"
|
||||
v-if="action.position === 'row'"
|
||||
@click="perform($event, action, row)"
|
||||
><i :class="action.icon" v-if="action.icon"></i> {{ action.text }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -56,17 +69,23 @@ export default class ListingComponent extends Component {
|
||||
data = []
|
||||
resource_class = {}
|
||||
|
||||
access_msg = ''
|
||||
can_access = false
|
||||
|
||||
async vue_on_create() {
|
||||
// Load the resource
|
||||
const resource_mod = await import(`../resource/${this.resource}.resource.js`)
|
||||
if ( !resource_mod )
|
||||
throw new Error('Unable to load Cobalt listing resource.')
|
||||
this.resource_class = await resource_service.get(this.resource)
|
||||
|
||||
const rsc_name = this.resource.toLowerCase().replace(/\//g, '_')
|
||||
if ( !resource_mod[rsc_name] )
|
||||
throw new Error('Unable to extract resource object from module.')
|
||||
// Make sure we have permission
|
||||
if ( !(await this.resource_class.can('list')) ) {
|
||||
this.access_msg = 'Sorry, you do not have permission to view this resource.'
|
||||
this.can_access = false
|
||||
return
|
||||
} else {
|
||||
this.access_msg = ''
|
||||
this.can_access = true
|
||||
}
|
||||
|
||||
this.resource_class = resource_mod[rsc_name]
|
||||
await this.load()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user