feat: admin UI fixes + SAML strategy + dashboard gql

This commit is contained in:
Nicolas Giard 2018-09-03 20:58:54 -04:00
parent 01f43bfaa5
commit 5e109802c6
11 changed files with 420 additions and 704 deletions

View File

@ -1,7 +1,7 @@
<template lang='pug'> <template lang='pug'>
v-app(:dark='darkMode').admin v-app(:dark='darkMode').admin
nav-header nav-header
v-navigation-drawer.pb-0(v-model='adminDrawerShown', app, fixed, clipped, left, permanent) v-navigation-drawer.pb-0.admin-sidebar(v-model='adminDrawerShown', app, fixed, clipped, left, permanent)
v-list(dense) v-list(dense)
v-list-tile.pt-2(to='/dashboard') v-list-tile.pt-2(to='/dashboard')
v-list-tile-avatar: v-icon dashboard v-list-tile-avatar: v-icon dashboard
@ -71,7 +71,7 @@
v-list-tile-avatar: v-icon favorite v-list-tile-avatar: v-icon favorite
v-list-tile-title {{ $t('admin:contribute.title') }} v-list-tile-title {{ $t('admin:contribute.title') }}
v-content v-content(:class='darkMode ? "grey darken-4" : ""')
transition(name='admin-router') transition(name='admin-router')
router-view router-view
@ -138,4 +138,20 @@ export default {
} }
} }
.admin-sidebar {
.v-list__tile--active {
background-color: rgba(mc('theme', 'primary'), .1);
.v-icon {
color: mc('theme', 'primary')
}
}
}
.theme--dark {
.admin-sidebar .v-list__tile--active {
background-color: rgba(0,0,0, .2);
}
}
</style> </style>

View File

@ -47,7 +47,7 @@
persistent-hint persistent-hint
:class='cfg.value.hint ? "mb-2" : ""' :class='cfg.value.hint ? "mb-2" : ""'
) )
v-switch( v-switch.mb-3(
v-else-if='cfg.value.type === "boolean"' v-else-if='cfg.value.type === "boolean"'
:key='cfg.key' :key='cfg.key'
:label='cfg.value.title' :label='cfg.value.title'

View File

@ -6,68 +6,142 @@
v-container(fluid, grid-list-lg) v-container(fluid, grid-list-lg)
v-layout(row, wrap) v-layout(row, wrap)
v-flex(xs12 md6 lg4 xl3 d-flex) v-flex(xs12 md6 lg4 xl3 d-flex)
v-card.primary(dark) v-card.primary.dashboard-card(dark)
v-card-text v-card-text
v-icon.dashboard-icon insert_drive_file v-icon.dashboard-icon insert_drive_file
.subheading Pages .subheading Pages
animated-number.display-1( animated-number.display-1(
:value='357' :value='info.pagesTotal'
:duration='2000' :duration='2000'
:formatValue='round' :formatValue='round'
easing='easeOutQuint' easing='easeOutQuint'
) )
v-flex(xs12 md6 lg4 xl3 d-flex) v-flex(xs12 md6 lg4 xl3 d-flex)
v-card.indigo.lighten-1(dark) v-card.indigo.lighten-1.dashboard-card(dark)
v-card-text v-card-text
v-icon.dashboard-icon person v-icon.dashboard-icon person
.subheading Users .subheading Users
animated-number.display-1( animated-number.display-1(
:value='34' :value='info.usersTotal'
:duration='2000' :duration='2000'
:formatValue='round' :formatValue='round'
easing='easeOutQuint' easing='easeOutQuint'
) )
v-flex(xs12 md6 lg4 xl3 d-flex) v-flex(xs12 md6 lg4 xl3 d-flex)
v-card.indigo.lighten-2(dark) v-card.indigo.lighten-2.dashboard-card(dark)
v-card-text v-card-text
v-icon.dashboard-icon people v-icon.dashboard-icon people
.subheading Groups .subheading Groups
animated-number.display-1( animated-number.display-1(
:value='5' :value='info.groupsTotal'
:duration='2000' :duration='2000'
:formatValue='round' :formatValue='round'
easing='easeOutQuint' easing='easeOutQuint'
) )
v-flex(xs12 md6 lg12 xl3 d-flex) v-flex(xs12 md6 lg12 xl3 d-flex)
v-card.teal.lighten-2(dark) v-card.dashboard-card(
:class='isLatestVersion ? "teal lighten-2" : "red lighten-2"'
dark
)
v-btn(fab, absolute, right, top, small, light, to='system')
v-icon(v-if='isLatestVersion', color='teal') build
v-icon(v-else, color='red darken-4') get_app
v-card-text v-card-text
v-icon.dashboard-icon blur_on v-icon.dashboard-icon blur_on
.subheading Wiki.js 2.0.0 .subheading Wiki.js {{info.currentVersion}}
.body-2 You are running the latest version. .body-2(v-if='isLatestVersion') You are running the latest version.
.body-2(v-else) A new version is available: {{info.latestVersion}}
v-flex(xs12) v-flex(xs12)
v-card v-card
v-card-text --- v-card-title.subheading Recent Pages
v-data-table.pb-2(
:items='recentPages'
hide-actions
hide-headers
)
template(slot='items' slot-scope='props')
td(width='20', style='padding-right: 0;'): v-icon insert_drive_file
td
.body-2.primary--text {{ props.item.title }}
.caption.grey--text.text--darken-2 {{ props.item.description }}
td.caption /{{ props.item.path }}
td.grey--text.text--darken-2(width='250')
.caption: strong Updated {{ props.item.updatedAt | moment('from') }}
.caption Created {{ props.item.createdAt | moment('calendar') }}
v-flex(xs12)
v-card
v-card-title.subheading Most Popular Pages
v-data-table.pb-2(
:items='popularPages'
hide-actions
hide-headers
)
template(slot='items' slot-scope='props')
td(width='20', style='padding-right: 0;'): v-icon insert_drive_file
td
.body-2.primary--text {{ props.item.title }}
.caption.grey--text.text--darken-2 {{ props.item.description }}
td.caption /{{ props.item.path }}
td.grey--text.text--darken-2(width='250')
.caption: strong Updated {{ props.item.updatedAt | moment('from') }}
.caption Created {{ props.item.createdAt | moment('calendar') }}
</template> </template>
<script> <script>
import AnimatedNumber from 'animated-number-vue' import AnimatedNumber from 'animated-number-vue'
import statsQuery from 'gql/admin/dashboard/dashboard-query-stats.gql'
export default { export default {
components: { components: {
AnimatedNumber AnimatedNumber
}, },
data() { data() {
return {} return {
info: {
currentVersion: 'n/a',
latestVersion: 'n/a',
groupsTotal: 0,
pagesTotal: 0,
usersTotal: 0
},
recentPages: [],
popularPages: []
}
},
computed: {
isLatestVersion() {
return this.info.currentVersion === this.info.latestVersion
}
}, },
methods: { methods: {
round(val) { return Math.round(val) } round(val) { return Math.round(val) }
},
apollo: {
info: {
query: statsQuery,
fetchPolicy: 'network-only',
update: (data) => data.system.info,
watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-system-refresh')
}
}
} }
} }
</script> </script>
<style lang='scss'> <style lang='scss'>
.dashboard-card {
display: flex;
.v-card__text {
overflow: hidden;
position: relative;
}
}
.dashboard-icon { .dashboard-icon {
position: absolute; position: absolute;
right: 0; right: 0;

View File

@ -14,7 +14,7 @@
) )
v-icon.mr-2 line_weight v-icon.mr-2 line_weight
.subheading Pipeline .subheading Pipeline
v-expansion-panel.adm-rendering-pipeline v-expansion-panel.adm-rendering-pipeline(v-model='selectedCore')
v-expansion-panel-content( v-expansion-panel-content(
hide-actions hide-actions
v-for='core in cores' v-for='core in cores'
@ -31,10 +31,11 @@
v-icon.mx-2 arrow_forward v-icon.mx-2 arrow_forward
.caption {{core.output}} .caption {{core.output}}
v-list(two-line, dense) v-list(two-line, dense)
template(v-for='(rdr, n) in core.children')
v-list-tile( v-list-tile(
avatar avatar
v-for='rdr in core.children'
:key='rdr.key' :key='rdr.key'
@click=''
) )
v-list-tile-avatar v-list-tile-avatar
v-icon(color='grey') {{rdr.icon}} v-icon(color='grey') {{rdr.icon}}
@ -44,7 +45,7 @@
v-list-tile-avatar v-list-tile-avatar
v-icon(color='green', small, v-if='rdr.isEnabled') lens v-icon(color='green', small, v-if='rdr.isEnabled') lens
v-icon(color='red', small, v-else) trip_origin v-icon(color='red', small, v-else) trip_origin
v-divider.my-0 v-divider.my-0(v-if='n < core.children.length - 1')
v-flex(lg9 xs12) v-flex(lg9 xs12)
v-card v-card
@ -112,6 +113,7 @@ import renderersQuery from 'gql/admin/rendering/rendering-query-renderers.gql'
export default { export default {
data() { data() {
return { return {
selectedCore: 0,
linkify: true, linkify: true,
codeTheme: 'Light', codeTheme: 'Light',
renderers: [] renderers: []
@ -125,6 +127,13 @@ export default {
}) })
} }
}, },
watch: {
renderers(newValue, oldValue) {
_.delay(() => {
this.selectedCore = _.findIndex(this.cores, ['key', 'markdownCore'])
}, 500)
}
},
apollo: { apollo: {
renderers: { renderers: {
query: renderersQuery, query: renderersQuery,

View File

@ -0,0 +1,11 @@
query {
system {
info {
currentVersion
latestVersion
groupsTotal
pagesTotal
usersTotal
}
}
}

View File

@ -127,6 +127,7 @@
"passport-oauth2": "1.4.0", "passport-oauth2": "1.4.0",
"passport-okta-oauth": "0.0.1", "passport-okta-oauth": "0.0.1",
"passport-openidconnect": "0.0.2", "passport-openidconnect": "0.0.2",
"passport-saml": "0.35.0",
"passport-slack": "0.0.7", "passport-slack": "0.0.7",
"passport-twitch": "1.0.3", "passport-twitch": "1.0.3",
"passport-windowslive": "1.0.2", "passport-windowslive": "1.0.2",

View File

@ -22,33 +22,77 @@ module.exports = {
async system() { return {} } async system() { return {} }
}, },
SystemQuery: { SystemQuery: {
async info(obj, args, context, info) { async info() { return {} }
},
SystemMutation: { },
SystemInfo: {
configFile() {
return path.join(process.cwd(), 'config.yml')
},
currentVersion() {
return WIKI.version
},
dbType() {
return _.get(dbTypes, WIKI.config.db.type, 'Unknown DB')
},
dbVersion() {
return _.get(WIKI.models, 'knex.client.version', 'Unknown version')
},
dbHost() {
return WIKI.config.db.host
},
latestVersion() {
return '2.0.0' // TODO
},
latestVersionReleaseDate() {
return new Date() // TODO
},
async operatingSystem() {
let osLabel = `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}` let osLabel = `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}`
if (os.platform() === 'linux') { if (os.platform() === 'linux') {
const osInfo = await getos() const osInfo = await getos()
osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}` osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}`
} }
return osLabel
return {
configFile: path.join(process.cwd(), 'config.yml'),
currentVersion: WIKI.version,
dbType: _.get(dbTypes, WIKI.config.db.type, 'Unknown DB'),
dbVersion: _.get(WIKI.models, 'knex.client.version', 'Unknown version'),
dbHost: WIKI.config.db.host,
latestVersion: WIKI.version, // TODO
latestVersionReleaseDate: new Date(), // TODO
operatingSystem: osLabel,
hostname: os.hostname(),
cpuCores: os.cpus().length,
ramTotal: filesize(os.totalmem()),
workingDirectory: process.cwd(),
nodeVersion: process.version.substr(1),
redisVersion: WIKI.redis.serverInfo.redis_version,
redisUsedRAM: WIKI.redis.serverInfo.used_memory_human,
redisTotalRAM: _.get(WIKI.redis.serverInfo, 'total_system_memory_human', 'N/A'),
redisHost: WIKI.redis.options.host
}
}
}, },
SystemMutation: { } hostname() {
return os.hostname()
},
cpuCores() {
return os.cpus().length
},
ramTotal() {
return filesize(os.totalmem())
},
workingDirectory() {
return process.cwd()
},
nodeVersion() {
return process.version.substr(1)
},
redisVersion() {
return WIKI.redis.serverInfo.redis_version
},
redisUsedRAM() {
return WIKI.redis.serverInfo.used_memory_human
},
redisTotalRAM() {
return _.get(WIKI.redis.serverInfo, 'total_system_memory_human', 'N/A')
},
redisHost() {
return WIKI.redis.options.host
},
async groupsTotal() {
const total = await WIKI.models.groups.query().count('* as total').first().pluck('total')
return _.toSafeInteger(total)
},
async pagesTotal() {
const total = await WIKI.models.pages.query().count('* as total').first().pluck('total')
return _.toSafeInteger(total)
},
async usersTotal() {
const total = await WIKI.models.users.query().count('* as total').first().pluck('total')
return _.toSafeInteger(total)
}
}
} }

View File

@ -37,15 +37,18 @@ type SystemInfo {
dbHost: String dbHost: String
dbType: String dbType: String
dbVersion: String dbVersion: String
groupsTotal: Int
hostname: String hostname: String
latestVersion: String latestVersion: String
latestVersionReleaseDate: Date latestVersionReleaseDate: Date
nodeVersion: String nodeVersion: String
operatingSystem: String operatingSystem: String
pagesTotal: Int
ramTotal: String ramTotal: String
redisHost: String redisHost: String
redisTotalRAM: String redisTotalRAM: String
redisUsedRAM: String redisUsedRAM: String
redisVersion: String redisVersion: String
usersTotal: Int
workingDirectory: String workingDirectory: String
} }

View File

@ -0,0 +1,40 @@
const _ = require('lodash')
/* global WIKI */
// ------------------------------------
// SAML Account
// ------------------------------------
const SAMLStrategy = require('passport-saml').Strategy
module.exports = {
init (passport, conf) {
passport.use('saml',
new SAMLStrategy({
callbackURL: conf.callbackURL,
entryPoint: conf.entryPoint,
issuer: conf.issuer,
audience: conf.audience,
cert: _.split(conf.cert, '|'),
privateCert: conf.privateCert,
decryptionPvk: conf.decryptionPvk,
signatureAlgorithm: conf.signatureAlgorithm,
identifierFormat: conf.identifierFormat,
acceptedClockSkewMs: _.toSafeInteger(conf.acceptedClockSkewMs),
disableRequestedAuthnContext: conf.disableRequestedAuthnContext,
authnContext: conf.authnContext,
forceAuthn: conf.forceAuthn,
providerName: conf.providerName,
skipRequestCompression: conf.skipRequestCompression,
authnRequestBinding: conf.authnRequestBinding
}, (profile, cb) => {
WIKI.models.users.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
})
)
}
}

View File

@ -0,0 +1,83 @@
key: saml
title: SAML 2.0
description: Security Assertion Markup Language 2.0 (SAML 2.0) is a version of the SAML standard for exchanging authentication and authorization data between security domains.
author: requarks.io
logo: https://static.requarks.io/logo/saml.svg
website: https://wiki.oasis-open.org/security/FrontPage
useForm: false
props:
entryPoint:
type: String
title: Entry Point
hint: Identity provider entrypoint (URL)
issuer:
type: String
title: Issuer
hint: Issuer string to supply to Identity Provider
audience:
type: String
title: Audience
hint: Expected SAML response Audience (if not provided, Audience won't be verified)
cert:
type: String
title: Certificate
hint: Public PEM-encoded X.509 signing certificate contents in base64 (e.g. 'MIICizCCAfQCCQCY8tKaMc0BMjANBgkqh ... W=='). If the provider has multiple certificates that are valid, join them together using the | pipe symbol.
privateCert:
type: String
title: Private Certificate
hint: PEM formatted key used to sign the certificate.
decryptionPvk:
type: String
title: Decryption Private Key
hint: (optional) Private key that will be used to attempt to decrypt any encrypted assertions that are received.
signatureAlgorithm:
type: String
title: Signature Algorithm
hint: Signature algorithm used for signing requests
default: sha1
enum:
- sha1
- sha256
- sha512
identifierFormat:
type: String
title: Name Identifier format
default: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
acceptedClockSkewMs:
type: Number
title: Accepted Clock Skew Milleseconds
hint: Time in milliseconds of skew that is acceptable between client and server when checking OnBefore and NotOnOrAfter assertion condition validity timestamps. Setting to -1 will disable checking these conditions entirely.
default: 0
disableRequestedAuthnContext:
type: Boolean
title: Disable Requested Auth Context
hint: If enabled, do not request a specific authentication context. This is known to help when authenticating against Active Directory (AD FS) servers.
default: false
authnContext:
type: String
title: Auth Context
hint: Name identifier format to request auth context.
default: urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
forceAuthn:
type: Boolean
title: Force Initial Re-authentication
hint: If enabled, the initial SAML request from the service provider specifies that the IdP should force re-authentication of the user, even if they possess a valid session.
default: false
providerName:
type: String
title: Provider Name
hint: Optional human-readable name of the requester for use by the presenter's user agent or the identity provider.
default: wiki.js
skipRequestCompression:
type: Boolean
title: Skip Request Compression
hint: If enabled, the SAML request from the service provider won't be compressed.
default: false
authnRequestBinding:
type: String
title: Request Binding
hint: Binding used for request authentication from IDP.
default: 'HTTP-Redirect'
enum:
- HTTP-Redirect
- HTTP-POST

739
yarn.lock

File diff suppressed because it is too large Load Diff