Commit 604941fe authored by Nick's avatar Nick

feat: utilities section (wip) + auth utilities

parent 9cd8657c
......@@ -83,8 +83,8 @@
v-list-tile(to='/system', v-if='hasPermission(`manage:system`)')
v-list-tile-avatar: v-icon tune
v-list-tile-title {{ $t('admin:system.title') }}
v-list-tile(to='/utilities', v-if='hasPermission(`manage:system`)', disabled)
v-list-tile-avatar: v-icon(color='grey lighten-2') build
v-list-tile(to='/utilities', v-if='hasPermission(`manage:system`)')
v-list-tile-avatar: v-icon build
v-list-tile-title {{ $t('admin:utilities.title') }}
v-list-tile(to='/webhooks', v-if='hasPermission(`manage:system`)', disabled)
v-list-tile-avatar: v-icon(color='grey lighten-2') ac_unit
......
<template lang='pug'>
v-card
v-toolbar(flat, color='primary', dark, dense)
.subheading {{ $t('admin:utilities.authTitle') }}
v-card-text
v-subheader.pl-0 Generate New Authentication Public / Private Key Certificates
.body-1 This will invalidate all current session tokens and cause all users to be logged out.
.body-1.red--text You will need to log back in after the operation.
v-btn(outline, color='primary', @click='regenCerts', :disabled='loading').ml-0.mt-3
v-icon(left) build
span Proceed
v-divider.my-3
v-subheader.pl-0 Reset Guest User
.body-1 This will reset the guest user to its default parameters and permissions.
v-btn(outline, color='primary', @click='resetGuest', :disabled='loading').ml-0.mt-3
v-icon(left) build
span Proceed
</template>
<script>
import _ from 'lodash'
import Cookies from 'js-cookie'
import utilityAuthRegencertsMutation from 'gql/admin/utilities/utilities-mutation-auth-regencerts.gql'
import utilityAuthResetguestMutation from 'gql/admin/utilities/utilities-mutation-auth-resetguest.gql'
export default {
data: () => {
return {
loading: false
}
},
methods: {
async regenCerts() {
this.loading = true
this.$store.commit(`loadingStart`, 'admin-utilities-auth-regencerts')
try {
const respRaw = await this.$apollo.mutate({
mutation: utilityAuthRegencertsMutation
})
const resp = _.get(respRaw, 'data.authentication.regenerateCertificates.responseResult', {})
if (resp.succeeded) {
this.$store.commit('showNotification', {
message: 'New Certificates generated successfully.',
style: 'success',
icon: 'check'
})
Cookies.remove('jwt')
_.delay(() => {
window.location.assign('/login')
}, 1000)
} else {
throw new Error(resp.message)
}
} catch (err) {
this.$store.commit('pushGraphError', err)
}
this.$store.commit(`loadingStop`, 'admin-utilities-auth-regencerts')
this.loading = false
},
async resetGuest() {
this.loading = true
this.$store.commit(`loadingStart`, 'admin-utilities-auth-resetguest')
try {
const respRaw = await this.$apollo.mutate({
mutation: utilityAuthResetguestMutation
})
const resp = _.get(respRaw, 'data.authentication.resetGuestUser.responseResult', {})
if (resp.succeeded) {
this.$store.commit('showNotification', {
message: 'Guest user was reset successfully.',
style: 'success',
icon: 'check'
})
} else {
throw new Error(resp.message)
}
} catch (err) {
this.$store.commit('pushGraphError', err)
}
this.$store.commit(`loadingStop`, 'admin-utilities-auth-resetguest')
this.loading = false
}
}
}
</script>
<style lang='scss'>
</style>
<template lang='pug'>
v-card
v-toolbar(flat, color='primary', dark, dense)
.subheading {{ $t('admin:utilities.cacheTitle') }}
v-card-text
v-subheader.pl-0 Flush Pages Cache
.body-1 Pages are cached to disk for better performance. You can flush the cache to force all content to be fetched from the DB again.
v-btn(outline, color='primary').ml-0.mt-3
v-icon(left) build
span Proceed
v-divider.my-3
v-subheader.pl-0 Flush Assets Cache
.body-1 Assets such as images and other files are cached to disk for better performance. You can flush the cache to force all assets to be fetched from the DB again.
v-btn(outline, color='primary').ml-0.mt-3
v-icon(left) build
span Proceed
v-divider.my-3
v-subheader.pl-0 Flush Temporary Uploads
.body-1 New uploads are temporarily saved to disk while they are being processed. They are automatically deleted after processing, but you can force an immediate cleanup using this tool.
v-btn(outline, color='primary').ml-0.mt-3
v-icon(left) build
span Proceed
</template>
<script>
export default {
}
</script>
<style lang='scss'>
</style>
<template lang='pug'>
v-card.wiki-form
v-toolbar(flat, color='primary', dark, dense)
.subheading {{ $t('admin:utilities.importv1Title') }}
v-card-text
.text-xs-center
img.animated.fadeInUp.wait-p1s(src='/svg/icon-software.svg')
.body-2 Import from Wiki.js 1.x
v-divider.my-4
.body-1 Data from a Wiki.js 1.x installation can be imported easily using this tool. What do you want to import?
v-checkbox(
label='Content'
value='content'
color='deep-orange darken-2'
v-model='importFilters'
hide-details
)
v-checkbox(
label='Uploads'
value='uploads'
color='deep-orange darken-2'
v-model='importFilters'
hide-details
)
v-checkbox(
label='Users'
value='users'
color='deep-orange darken-2'
v-model='importFilters'
hide-details
)
v-divider.my-3
v-text-field.mt-3(
outline
label='MongoDB Connection String'
hint='The connection string to connect to the Wiki.js 1.x MongoDB database.'
persistent-hint
v-model='dbConnStr'
v-if='needDB'
)
v-text-field.mt-3(
outline
label='Content Repo Path'
hint='The full path to where the Wiki.js 1.x content is stored on disk.'
persistent-hint
v-model='contentPath'
v-if='needDisk'
)
v-card-chin
v-btn(depressed, color='deep-orange darken-2', :disabled='!needDB && !needDisk').ml-0
v-icon(left, color='white') label_important
span.white--text Start Import
</template>
<script>
export default {
data() {
return {
importFilters: ['content', 'uploads'],
dbConnStr: 'mongodb://',
contentPath: '/wiki-v1/repo'
}
},
computed: {
needDB() {
return this.importFilters.indexOf('users') >= 0
},
needDisk() {
return this.importFilters.indexOf('content') >= 0 || this.importFilters.indexOf('uploads') >= 0
}
}
}
</script>
<style lang='scss'>
</style>
<template lang='pug'>
v-card
v-toolbar(flat, color='primary', dark, dense)
.subheading {{ $t('admin:utilities.telemetryTitle') }}
v-form
v-card-text
v-subheader What is telemetry?
.body-1.pl-3 Telemetry allows the developers of Wiki.js to improve the software by collecting basic anonymized data about its usage and the host info. #[br] This is entirely optional and #[strong absolutely no] private data (such as content or personal data) is collected.
.body-1.pt-3.pl-3 For maximum privacy, a random client ID is generated during setup. This ID is used to group requests together while keeping complete anonymity. You can reset and generate a new one below at any time.
v-divider.my-3
v-subheader What is collected?
.body-1.pl-3 When telemetry is enabled, only the following data is transmitted:
v-list
v-list-tile
v-list-tile-avatar: v-icon info_outline
v-list-tile-content
v-list-tile-title.body-1 Version of Wiki.js installed
v-list-tile-subtitle.caption: em e.g. v2.0.123
v-list-tile
v-list-tile-avatar: v-icon info_outline
v-list-tile-content
v-list-tile-title.body-1 Basic OS information
v-list-tile-subtitle.caption: em Platform (Linux, macOS or Windows), Total CPU cores and DB type (PostgreSQL, MySQL, MariaDB, SQLite or SQL Server)
v-list-tile
v-list-tile-avatar: v-icon info_outline
v-list-tile-content
v-list-tile-title.body-1 Crash debug data
v-list-tile-subtitle.caption: em Stack trace of the error
v-list-tile
v-list-tile-avatar: v-icon info_outline
v-list-tile-content
v-list-tile-title.body-1 Setup analytics
v-list-tile-subtitle.caption: em Installation checkpoint reached
.body-1.pl-3 Note that any data collected is stored for a maximum of 180 days, after which it is permanently deleted.
v-divider.my-3
v-subheader What is it used for?
.body-1.pl-3 Telemetry is used by developers to improve Wiki.js, mostly for the following reasons:
v-list(dense)
v-list-tile
v-list-tile-avatar: v-icon info_outline
v-list-tile-content: v-list-tile-title.body-1 Identify critical bugs more easily and fix them in a timely manner.
v-list-tile
v-list-tile-avatar: v-icon info_outline
v-list-tile-content: v-list-tile-title.body-1 Understand the upgrade rate of current installations.
v-list-tile
v-list-tile-avatar: v-icon info_outline
v-list-tile-content: v-list-tile-title.body-1 Optimize performance and testing scenarios based on most popular environments.
.body-1.pl-3 Only authorized developers have access to the data. It is not shared to any 3rd party nor is it used for any other application than improving Wiki.js.
v-divider.my-3
v-subheader Settings
.pl-3
v-switch.mt-0(
v-model='telemetry',
label='Enable Telemetry',
:value='true',
color='primary',
hint='Allow Wiki.js to transmit telemetry data.',
persistent-hint
)
.subheading.mt-3.grey--text.text--darken-1 Client ID
.body-1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
v-card-chin
v-btn(depressed, color='success', @click='updateTelemetry')
v-icon(left) chevron_right
| Save Changes
v-spacer
v-btn(outline, color='grey', @click='resetClientId')
v-icon(left) autorenew
span Reset Client ID
</template>
<script>
export default {
}
</script>
<style lang='scss'>
</style>
mutation {
authentication {
regenerateCertificates {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
mutation {
authentication {
resetGuestUser {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve" width="96px" height="96px">
<path style="fill:#891212;" d="M24,28L6,35V20l18-7V28z"/>
<path style="fill:#891212;" d="M42,20l-18-7v15l18,7V20z"/>
<path style="fill:#891212;" d="M26,18.5c0,1.379-1.121,2.5-2.5,2.5S21,19.879,21,18.5s1.121-2.5,2.5-2.5S26,17.121,26,18.5z"/>
<path style="fill:#90CAF9;" d="M23.5,6C16.598,6,11,11.598,11,18.5S16.598,31,23.5,31S36,25.402,36,18.5S30.402,6,23.5,6z M23.5,20.805c-1.273,0-2.305-1.031-2.305-2.305s1.031-2.305,2.305-2.305s2.305,1.031,2.305,2.305S24.77,20.805,23.5,20.805z"/>
<path style="fill:#CFE8F9;" d="M20.676,19.844L13,25.344c0.879,1.316,2.516,2.938,4.188,4l5.43-7.531 C21.684,21.516,20.957,20.781,20.676,19.844z"/>
<path style="fill:#CFE8F9;" d="M30.27,8l-5.965,8.164c0.938,0.266,1.68,0.977,1.988,1.895l8.117-5.648 C33.41,10.621,31.984,9.109,30.27,8z"/>
<path style="fill:#F44336;" d="M24,27L6,20v15l18,7V27z"/>
<path style="fill:#C62828;" d="M42,35l-18,7V27l18-7V35z"/>
<path style="fill:#1E88E5;" d="M23.5,15c-1.934,0-3.5,1.566-3.5,3.5s1.566,3.5,3.5,3.5s3.5-1.566,3.5-3.5S25.434,15,23.5,15z M23.5,20c-0.828,0-1.5-0.672-1.5-1.5s0.672-1.5,1.5-1.5s1.5,0.672,1.5,1.5S24.328,20,23.5,20z"/>
</svg>
......@@ -46,12 +46,12 @@ const init = {
atomic: 400
})
devWatcher.on('ready', () => {
devWatcher.on('all', () => {
devWatcher.on('all', _.debounce(() => {
console.warn(chalk.yellow.bold('--- >>>>>>>>>>>>>>>>>>>>>>>>>>>> ---'))
console.warn(chalk.yellow.bold('--- Changes detected: Restarting ---'))
console.warn(chalk.yellow.bold('--- <<<<<<<<<<<<<<<<<<<<<<<<<<<< ---'))
this.reload()
})
}, 500))
})
})
},
......
......@@ -4,6 +4,9 @@ const _ = require('lodash')
const path = require('path')
const jwt = require('jsonwebtoken')
const moment = require('moment')
const Promise = require('bluebird')
const crypto = Promise.promisifyAll(require('crypto'))
const pem2jwk = require('pem-jwk').pem2jwk
const securityHelper = require('../helpers/security')
......@@ -236,5 +239,72 @@ module.exports = {
async reloadGroups() {
const groupsArray = await WIKI.models.groups.query()
this.groups = _.keyBy(groupsArray, 'id')
},
/**
* Generate New Authentication Public / Private Key Certificates
*/
async regenerateCertificates() {
WIKI.logger.info('Regenerating certificates...')
_.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
const certs = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs1',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: WIKI.config.sessionSecret
}
})
_.set(WIKI.config, 'certs', {
jwk: pem2jwk(certs.publicKey),
public: certs.publicKey,
private: certs.privateKey
})
await WIKI.configSvc.saveToDb([
'certs',
'sessionSecret'
])
await WIKI.auth.activateStrategies()
WIKI.logger.info('Regenerated certificates: [ COMPLETED ]')
},
/**
* Reset Guest User
*/
async resetGuestUser() {
WIKI.logger.info('Resetting guest account...')
const guestGroup = await WIKI.models.groups.query().where('id', 2).first()
await WIKI.models.users.query().delete().where({
providerKey: 'local',
email: 'guest@example.com'
}).orWhere('id', 2)
const guestUser = await WIKI.models.users.query().insert({
id: 2,
provider: 'local',
email: 'guest@example.com',
name: 'Guest',
password: '',
locale: 'en',
defaultEditor: 'markdown',
tfaIsActive: false,
isSystem: true,
isActive: true,
isVerified: true
})
await guestUser.$relatedQuery('groups').relate(guestGroup.id)
WIKI.logger.info('Guest user has been reset: [ COMPLETED ]')
}
}
......@@ -13,6 +13,9 @@ module.exports = {
async authentication() { return {} }
},
AuthenticationQuery: {
/**
* Fetch active authentication strategies
*/
async strategies(obj, args, context, info) {
let strategies = await WIKI.models.authentication.getStrategies(args.isEnabled)
strategies = strategies.map(stg => {
......@@ -38,6 +41,9 @@ module.exports = {
}
},
AuthenticationMutation: {
/**
* Perform Login
*/
async login(obj, args, context) {
try {
const authResult = await WIKI.models.users.login(args, context)
......@@ -54,6 +60,9 @@ module.exports = {
return graphHelper.generateError(err)
}
},
/**
* Perform 2FA Login
*/
async loginTFA(obj, args, context) {
try {
const authResult = await WIKI.models.users.loginTFA(args, context)
......@@ -65,6 +74,9 @@ module.exports = {
return graphHelper.generateError(err)
}
},
/**
* Register a new account
*/
async register(obj, args, context) {
try {
await WIKI.models.users.register({ ...args, verify: true }, context)
......@@ -75,6 +87,9 @@ module.exports = {
return graphHelper.generateError(err)
}
},
/**
* Update Authentication Strategies
*/
async updateStrategies(obj, args, context) {
try {
WIKI.config.auth = {
......@@ -103,6 +118,32 @@ module.exports = {
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* Generate New Authentication Public / Private Key Certificates
*/
async regenerateCertificates(obj, args, context) {
try {
await WIKI.auth.regenerateCertificates()
return {
responseResult: graphHelper.generateSuccess('Certificates have been regenerated successfully.')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* Reset Guest User
*/
async resetGuestUser(obj, args, context) {
try {
await WIKI.auth.resetGuestUser()
return {
responseResult: graphHelper.generateSuccess('Guest user has been reset successfully.')
}
} catch (err) {
return graphHelper.generateError(err)
}
}
},
AuthenticationStrategy: {
......
......@@ -46,6 +46,9 @@ type AuthenticationMutation {
strategies: [AuthenticationStrategyInput]!
config: AuthenticationConfigInput
): DefaultResponse @auth(requires: ["manage:system"])
regenerateCertificates: DefaultResponse @auth(requires: ["manage:system"])
resetGuestUser: DefaultResponse @auth(requires: ["manage:system"])
}
# -----------------------------------------------
......
......@@ -336,7 +336,7 @@ module.exports = class User extends Model {
static async loginTFA(opts, context) {
if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
let result = null // await WIKI.redis.get(`tfa:${opts.loginToken}`)
let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
if (result) {
let userId = _.toSafeInteger(result)
if (userId && userId > 0) {
......
......@@ -351,7 +351,7 @@ module.exports = {
new stream.Transform({
objectMode: true,
transform: async (page, enc, cb) => {
const fileName = `${page.path}.${getFileExtension(page.contentType)}`
let fileName = `${page.path}.${getFileExtension(page.contentType)}`
if (WIKI.config.lang.namespacing && WIKI.config.lang.code !== page.localeCode) {
fileName = `${page.localeCode}/${fileName}`
}
......
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 to comment