feat: login + TFA authentication
This commit is contained in:
parent
85717bd369
commit
cb0d86906f
@ -1,20 +1,29 @@
|
|||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
.login(:class='{ "is-error": error }')
|
.login(:class='{ "is-error": error }')
|
||||||
.login-container(:class='{ "is-expanded": strategies.length > 1 }')
|
.login-container(:class='{ "is-expanded": strategies.length > 1, "is-loading": isLoading }')
|
||||||
.login-providers(v-show='strategies.length > 1')
|
.login-providers(v-show='strategies.length > 1')
|
||||||
button(v-for='strategy in strategies', :class='{ "is-active": strategy.key === selectedStrategy }', @click='selectStrategy(strategy.key, strategy.useForm)', :title='strategy.title')
|
button(v-for='strategy in strategies', :class='{ "is-active": strategy.key === selectedStrategy }', @click='selectStrategy(strategy.key, strategy.useForm)', :title='strategy.title')
|
||||||
em(v-html='strategy.icon')
|
em(v-html='strategy.icon')
|
||||||
span {{ strategy.title }}
|
span {{ strategy.title }}
|
||||||
.login-providers-fill
|
.login-providers-fill
|
||||||
.login-frame
|
.login-frame(v-show='screen === "login"')
|
||||||
h1 {{ siteTitle }}
|
h1 {{ siteTitle }}
|
||||||
h2 {{ $t('auth:loginrequired') }}
|
h2 {{ $t('auth:loginRequired') }}
|
||||||
input(type='text', ref='iptEmail', :placeholder='$t("auth:fields.emailuser")')
|
input(type='text', ref='iptEmail', v-model='username', :placeholder='$t("auth:fields.emailUser")')
|
||||||
input(type='password', ref='iptPassword', :placeholder='$t("auth:fields.password")')
|
input(type='password', ref='iptPassword', v-model='password', :placeholder='$t("auth:fields.password")', @keyup.enter='login')
|
||||||
button.button.is-blue.is-fullwidth(@click='login')
|
button.button.is-blue.is-fullwidth(@click='login')
|
||||||
span {{ $t('auth:actions.login') }}
|
span {{ $t('auth:actions.login') }}
|
||||||
|
.login-frame(v-show='screen === "tfa"')
|
||||||
|
.login-frame-icon
|
||||||
|
svg.icons.is-48(role='img')
|
||||||
|
title {{ $t('auth:tfa.title') }}
|
||||||
|
use(xlink:href='#nc-key')
|
||||||
|
h2 {{ $t('auth:tfa.subtitle') }}
|
||||||
|
input(type='text', ref='iptTFA', v-model='securityCode', :placeholder='$t("auth:tfa.placeholder")', @keyup.enter='verifySecurityCode')
|
||||||
|
button.button.is-blue.is-fullwidth(@click='verifySecurityCode')
|
||||||
|
span {{ $t('auth:tfa.verifyToken') }}
|
||||||
.login-copyright
|
.login-copyright
|
||||||
span {{ $t('footer.poweredby') }}
|
span {{ $t('footer.poweredBy') }}
|
||||||
a(href='https://wiki.js.org', rel='external', title='Wiki.js') Wiki.js
|
a(href='https://wiki.js.org', rel='external', title='Wiki.js') Wiki.js
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -27,7 +36,13 @@ export default {
|
|||||||
return {
|
return {
|
||||||
error: false,
|
error: false,
|
||||||
strategies: [],
|
strategies: [],
|
||||||
selectedStrategy: 'local'
|
selectedStrategy: 'local',
|
||||||
|
screen: 'login',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
securityCode: '',
|
||||||
|
loginToken: '',
|
||||||
|
isLoading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -38,11 +53,15 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
selectStrategy (key, useForm) {
|
selectStrategy (key, useForm) {
|
||||||
this.selectedStrategy = key
|
this.selectedStrategy = key
|
||||||
|
this.screen = 'login'
|
||||||
if (!useForm) {
|
if (!useForm) {
|
||||||
window.location.assign(siteConfig.path + '/login/' + key)
|
window.location.assign(siteConfig.path + 'login/' + key)
|
||||||
|
} else {
|
||||||
|
this.$refs.iptEmail.focus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshStrategies () {
|
refreshStrategies () {
|
||||||
|
this.isLoading = true
|
||||||
graphQL.query({
|
graphQL.query({
|
||||||
query: CONSTANTS.GRAPHQL.GQL_QUERY_AUTHENTICATION,
|
query: CONSTANTS.GRAPHQL.GQL_QUERY_AUTHENTICATION,
|
||||||
variables: {
|
variables: {
|
||||||
@ -54,16 +73,119 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('No authentication providers available!')
|
throw new Error('No authentication providers available!')
|
||||||
}
|
}
|
||||||
|
this.isLoading = false
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
|
||||||
},
|
|
||||||
login() {
|
|
||||||
this.$store.dispatch('alert', {
|
this.$store.dispatch('alert', {
|
||||||
style: 'error',
|
style: 'error',
|
||||||
icon: 'gg-warning',
|
icon: 'gg-warning',
|
||||||
msg: 'Email or password is invalid'
|
msg: err.message
|
||||||
})
|
})
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
login () {
|
||||||
|
if (this.username.length < 2) {
|
||||||
|
this.$store.dispatch('alert', {
|
||||||
|
style: 'error',
|
||||||
|
icon: 'gg-warning',
|
||||||
|
msg: 'Enter a valid email / username.'
|
||||||
|
})
|
||||||
|
this.$refs.iptEmail.focus()
|
||||||
|
} else if (this.password.length < 2) {
|
||||||
|
this.$store.dispatch('alert', {
|
||||||
|
style: 'error',
|
||||||
|
icon: 'gg-warning',
|
||||||
|
msg: 'Enter a valid password.'
|
||||||
|
})
|
||||||
|
this.$refs.iptPassword.focus()
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
graphQL.mutate({
|
||||||
|
mutation: CONSTANTS.GRAPHQL.GQL_MUTATION_LOGIN,
|
||||||
|
variables: {
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
provider: this.selectedStrategy
|
||||||
|
}
|
||||||
|
}).then(resp => {
|
||||||
|
if (resp.data.login) {
|
||||||
|
let respObj = resp.data.login
|
||||||
|
if (respObj.succeeded === true) {
|
||||||
|
if (respObj.tfaRequired === true) {
|
||||||
|
this.screen = 'tfa'
|
||||||
|
this.securityCode = ''
|
||||||
|
this.loginToken = respObj.tfaLoginToken
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.iptTFA.focus()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$store.dispatch('alert', {
|
||||||
|
style: 'success',
|
||||||
|
icon: 'gg-check',
|
||||||
|
msg: 'Login successful!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.isLoading = false
|
||||||
|
} else {
|
||||||
|
throw new Error(respObj.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Authentication is unavailable.')
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
this.$store.dispatch('alert', {
|
||||||
|
style: 'error',
|
||||||
|
icon: 'gg-warning',
|
||||||
|
msg: err.message
|
||||||
|
})
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verifySecurityCode () {
|
||||||
|
if (this.securityCode.length !== 6) {
|
||||||
|
this.$store.dispatch('alert', {
|
||||||
|
style: 'error',
|
||||||
|
icon: 'gg-warning',
|
||||||
|
msg: 'Enter a valid security code.'
|
||||||
|
})
|
||||||
|
this.$refs.iptTFA.focus()
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
graphQL.mutate({
|
||||||
|
mutation: CONSTANTS.GRAPHQL.GQL_MUTATION_LOGINTFA,
|
||||||
|
variables: {
|
||||||
|
loginToken: this.loginToken,
|
||||||
|
securityCode: this.securityCode
|
||||||
|
}
|
||||||
|
}).then(resp => {
|
||||||
|
if (resp.data.loginTFA) {
|
||||||
|
let respObj = resp.data.loginTFA
|
||||||
|
if (respObj.succeeded === true) {
|
||||||
|
this.$store.dispatch('alert', {
|
||||||
|
style: 'success',
|
||||||
|
icon: 'gg-check',
|
||||||
|
msg: 'Login successful!'
|
||||||
|
})
|
||||||
|
this.isLoading = false
|
||||||
|
} else {
|
||||||
|
throw new Error(respObj.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Authentication is unavailable.')
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
this.$store.dispatch('alert', {
|
||||||
|
style: 'error',
|
||||||
|
icon: 'gg-warning',
|
||||||
|
msg: err.message
|
||||||
|
})
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -18,5 +18,23 @@ export default {
|
|||||||
value
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
GQL_MUTATION_LOGIN: gql`
|
||||||
|
mutation($username: String!, $password: String!, $provider: String!) {
|
||||||
|
login(username: $username, password: $password, provider: $provider) {
|
||||||
|
succeeded
|
||||||
|
message
|
||||||
|
tfaRequired
|
||||||
|
tfaLoginToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
GQL_MUTATION_LOGINTFA: gql`
|
||||||
|
mutation($loginToken: String!, $securityCode: String!) {
|
||||||
|
loginTFA(loginToken: $loginToken, securityCode: $securityCode) {
|
||||||
|
succeeded
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,15 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
animation: zoomIn .5s ease;
|
animation: zoomIn .5s ease;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
content: " ";
|
||||||
|
@include spinner(mc('blue', '500'),0.5s,16px);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
&.is-expanded {
|
&.is-expanded {
|
||||||
width: 650px;
|
width: 650px;
|
||||||
|
|
||||||
@ -67,6 +76,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-loading::after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
@include until($tablet) {
|
@include until($tablet) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@ -264,6 +277,16 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-tfa {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 400px;
|
||||||
|
align-items: stretch;
|
||||||
|
box-shadow: 0 14px 28px rgba(0,0,0,0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
animation: zoomIn .5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
&-copyright {
|
&-copyright {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -53,6 +53,9 @@ configNamespaces:
|
|||||||
- site
|
- site
|
||||||
- theme
|
- theme
|
||||||
- uploads
|
- uploads
|
||||||
|
localeNamespaces:
|
||||||
|
- auth
|
||||||
|
- common
|
||||||
queues:
|
queues:
|
||||||
- gitSync
|
- gitSync
|
||||||
- uplClearTemp
|
- uplClearTemp
|
||||||
|
@ -17,7 +17,12 @@ module.exports = {
|
|||||||
usernameField: 'email',
|
usernameField: 'email',
|
||||||
passwordField: 'password'
|
passwordField: 'password'
|
||||||
}, (uEmail, uPassword, done) => {
|
}, (uEmail, uPassword, done) => {
|
||||||
wiki.db.User.findOne({ email: uEmail, provider: 'local' }).then((user) => {
|
wiki.db.User.findOne({
|
||||||
|
where: {
|
||||||
|
email: uEmail,
|
||||||
|
provider: 'local'
|
||||||
|
}
|
||||||
|
}).then((user) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
return user.validatePassword(uPassword).then(() => {
|
return user.validatePassword(uPassword).then(() => {
|
||||||
return done(null, user) || true
|
return done(null, user) || true
|
||||||
@ -25,7 +30,7 @@ module.exports = {
|
|||||||
return done(err, null)
|
return done(err, null)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return done(new Error('INVALID_LOGIN'), null)
|
return done(new wiki.Error.AuthLoginFailed(), null)
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
done(err, null)
|
done(err, null)
|
||||||
|
30
server/helpers/error.js
Normal file
30
server/helpers/error.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
class BaseError extends Error {
|
||||||
|
constructor (message) {
|
||||||
|
super(message)
|
||||||
|
this.name = this.constructor.name
|
||||||
|
Error.captureStackTrace(this, this.constructor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthGenericError extends BaseError { constructor (message = 'An unexpected error occured during login.') { super(message) } }
|
||||||
|
class AuthLoginFailed extends BaseError { constructor (message = 'Invalid email / username or password.') { super(message) } }
|
||||||
|
class AuthProviderInvalid extends BaseError { constructor (message = 'Invalid authentication provider.') { super(message) } }
|
||||||
|
class AuthTFAFailed extends BaseError { constructor (message = 'Incorrect TFA Security Code.') { super(message) } }
|
||||||
|
class AuthTFAInvalid extends BaseError { constructor (message = 'Invalid TFA Security Code or Login Token.') { super(message) } }
|
||||||
|
class BruteInstanceIsInvalid extends BaseError { constructor (message = 'Invalid Brute Force Instance.') { super(message) } }
|
||||||
|
class BruteTooManyAttempts extends BaseError { constructor (message = 'Too many attempts! Try again later.') { super(message) } }
|
||||||
|
class LocaleInvalidNamespace extends BaseError { constructor (message = 'Invalid locale or namespace.') { super(message) } }
|
||||||
|
class UserCreationFailed extends BaseError { constructor (message = 'An unexpected error occured during user creation.') { super(message) } }
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
BaseError,
|
||||||
|
AuthGenericError,
|
||||||
|
AuthLoginFailed,
|
||||||
|
AuthProviderInvalid,
|
||||||
|
AuthTFAFailed,
|
||||||
|
AuthTFAInvalid,
|
||||||
|
BruteInstanceIsInvalid,
|
||||||
|
BruteTooManyAttempts,
|
||||||
|
LocaleInvalidNamespace,
|
||||||
|
UserCreationFailed
|
||||||
|
}
|
@ -1,15 +1,25 @@
|
|||||||
'use strict'
|
const Promise = require('bluebird')
|
||||||
|
const crypto = require('crypto')
|
||||||
/* global appdata, appconfig */
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sanitizeCommitUser (user) {
|
sanitizeCommitUser (user) {
|
||||||
let wlist = new RegExp('[^a-zA-Z0-9-_.\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g')
|
// let wlist = new RegExp('[^a-zA-Z0-9-_.\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g')
|
||||||
return {
|
// return {
|
||||||
name: _.chain(user.name).replace(wlist, '').trim().value(),
|
// name: _.chain(user.name).replace(wlist, '').trim().value(),
|
||||||
email: appconfig.git.showUserEmail ? user.email : appconfig.git.serverEmail
|
// email: appconfig.git.showUserEmail ? user.email : appconfig.git.serverEmail
|
||||||
}
|
// }
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Generate a random token
|
||||||
|
*
|
||||||
|
* @param {any} length
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async generateToken (length) {
|
||||||
|
return Promise.fromCallback(clb => {
|
||||||
|
crypto.randomBytes(length, clb)
|
||||||
|
}).then(buf => {
|
||||||
|
return buf.toString('hex')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ let wiki = {
|
|||||||
IS_MASTER: cluster.isMaster,
|
IS_MASTER: cluster.isMaster,
|
||||||
ROOTPATH: process.cwd(),
|
ROOTPATH: process.cwd(),
|
||||||
SERVERPATH: path.join(process.cwd(), 'server'),
|
SERVERPATH: path.join(process.cwd(), 'server'),
|
||||||
|
Error: require('./helpers/error'),
|
||||||
configSvc: require('./modules/config'),
|
configSvc: require('./modules/config'),
|
||||||
kernel: require('./modules/kernel')
|
kernel: require('./modules/kernel')
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,17 @@ module.exports = async () => {
|
|||||||
|
|
||||||
app.use('/', ctrl.auth)
|
app.use('/', ctrl.auth)
|
||||||
|
|
||||||
app.use('/graphql', graphqlApollo.graphqlExpress({ schema: graphqlSchema }))
|
app.use('/graphql', (req, res, next) => {
|
||||||
|
graphqlApollo.graphqlExpress({
|
||||||
|
schema: graphqlSchema,
|
||||||
|
context: { req, res },
|
||||||
|
formatError: (err) => {
|
||||||
|
return {
|
||||||
|
message: err.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(req, res, next)
|
||||||
|
})
|
||||||
app.use('/graphiql', graphqlApollo.graphiqlExpress({ endpointURL: '/graphql' }))
|
app.use('/graphiql', graphqlApollo.graphiqlExpress({ endpointURL: '/graphql' }))
|
||||||
// app.use('/uploads', mw.auth, ctrl.uploads)
|
// app.use('/uploads', mw.auth, ctrl.uploads)
|
||||||
app.use('/admin', mw.auth, ctrl.admin)
|
app.use('/admin', mw.auth, ctrl.admin)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
/* global wiki, appconfig */
|
/* global wiki */
|
||||||
|
|
||||||
const Promise = require('bluebird')
|
const Promise = require('bluebird')
|
||||||
const bcrypt = require('bcryptjs-then')
|
const bcrypt = require('bcryptjs-then')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
|
const tfa = require('node-2fa')
|
||||||
|
const securityHelper = require('../helpers/security')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Users schema
|
* Users schema
|
||||||
@ -56,10 +58,108 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
userSchema.prototype.validatePassword = function (rawPwd) {
|
userSchema.prototype.validatePassword = async function (rawPwd) {
|
||||||
return bcrypt.compare(rawPwd, this.password).then((isValid) => {
|
if (await bcrypt.compare(rawPwd, this.password) === true) {
|
||||||
return (isValid) ? true : Promise.reject(new Error(wiki.lang.t('auth:errors:invalidlogin')))
|
return true
|
||||||
|
} else {
|
||||||
|
throw new wiki.Error.AuthLoginFailed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userSchema.prototype.enableTFA = async function () {
|
||||||
|
let tfaInfo = tfa.generateSecret({
|
||||||
|
name: wiki.config.site.title
|
||||||
})
|
})
|
||||||
|
this.tfaIsActive = true
|
||||||
|
this.tfaSecret = tfaInfo.secret
|
||||||
|
return this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
userSchema.prototype.disableTFA = async function () {
|
||||||
|
this.tfaIsActive = false
|
||||||
|
this.tfaSecret = ''
|
||||||
|
return this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
userSchema.prototype.verifyTFA = function (code) {
|
||||||
|
let result = tfa.verifyToken(this.tfaSecret, code)
|
||||||
|
console.info(result)
|
||||||
|
return (result && _.has(result, 'delta') && result.delta === 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
userSchema.login = async (opts, context) => {
|
||||||
|
if (_.has(wiki.config.auth.strategies, opts.provider)) {
|
||||||
|
_.set(context.req, 'body.email', opts.username)
|
||||||
|
_.set(context.req, 'body.password', opts.password)
|
||||||
|
|
||||||
|
// Authenticate
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wiki.auth.passport.authenticate(opts.provider, async (err, user, info) => {
|
||||||
|
if (err) { return reject(err) }
|
||||||
|
if (!user) { return reject(new wiki.Error.AuthLoginFailed()) }
|
||||||
|
|
||||||
|
// Is 2FA required?
|
||||||
|
if (user.tfaIsActive) {
|
||||||
|
try {
|
||||||
|
let loginToken = await securityHelper.generateToken(32)
|
||||||
|
await wiki.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
|
||||||
|
return resolve({
|
||||||
|
succeeded: true,
|
||||||
|
message: 'Login Successful. Awaiting 2FA security code.',
|
||||||
|
tfaRequired: true,
|
||||||
|
tfaLoginToken: loginToken
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
wiki.logger.warn(err)
|
||||||
|
return reject(new wiki.Error.AuthGenericError())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No 2FA, log in user
|
||||||
|
return context.req.logIn(user, err => {
|
||||||
|
if (err) { return reject(err) }
|
||||||
|
resolve({
|
||||||
|
succeeded: true,
|
||||||
|
message: 'Login Successful',
|
||||||
|
tfaRequired: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})(context.req, context.res, () => {})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw new wiki.Error.AuthProviderInvalid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userSchema.loginTFA = async (opts, context) => {
|
||||||
|
if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
|
||||||
|
console.info(opts.loginToken)
|
||||||
|
let result = await wiki.redis.get(`tfa:${opts.loginToken}`)
|
||||||
|
console.info(result)
|
||||||
|
if (result) {
|
||||||
|
console.info('DUDE2')
|
||||||
|
let userId = _.toSafeInteger(result)
|
||||||
|
if (userId && userId > 0) {
|
||||||
|
console.info('DUDE3')
|
||||||
|
let user = await wiki.db.User.findById(userId)
|
||||||
|
if (user && user.verifyTFA(opts.securityCode)) {
|
||||||
|
console.info('DUDE4')
|
||||||
|
return Promise.fromCallback(clb => {
|
||||||
|
context.req.logIn(user, clb)
|
||||||
|
}).return({
|
||||||
|
succeeded: true,
|
||||||
|
message: 'Login Successful'
|
||||||
|
}).catch(err => {
|
||||||
|
wiki.logger.warn(err)
|
||||||
|
throw new wiki.Error.AuthGenericError()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw new wiki.Error.AuthTFAFailed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new wiki.Error.AuthTFAInvalid()
|
||||||
}
|
}
|
||||||
|
|
||||||
userSchema.processProfile = (profile) => {
|
userSchema.processProfile = (profile) => {
|
||||||
@ -92,7 +192,7 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
new: true
|
new: true
|
||||||
}).then((user) => {
|
}).then((user) => {
|
||||||
// Handle unregistered accounts
|
// Handle unregistered accounts
|
||||||
if (!user && profile.provider !== 'local' && (appconfig.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
|
if (!user && profile.provider !== 'local' && (wiki.config.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
|
||||||
let nUsr = {
|
let nUsr = {
|
||||||
email: primaryEmail,
|
email: primaryEmail,
|
||||||
provider: profile.provider,
|
provider: profile.provider,
|
||||||
|
@ -13,7 +13,7 @@ module.exports = {
|
|||||||
// Serialization user methods
|
// Serialization user methods
|
||||||
|
|
||||||
passport.serializeUser(function (user, done) {
|
passport.serializeUser(function (user, done) {
|
||||||
done(null, user._id)
|
done(null, user.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
passport.deserializeUser(function (id, done) {
|
passport.deserializeUser(function (id, done) {
|
||||||
|
@ -9,8 +9,9 @@ const Promise = require('bluebird')
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
engine: null,
|
engine: null,
|
||||||
namespaces: ['common', 'admin', 'auth', 'errors', 'git'],
|
namespaces: [],
|
||||||
init() {
|
init() {
|
||||||
|
this.namespaces = wiki.data.localeNamespaces
|
||||||
this.engine = i18next
|
this.engine = i18next
|
||||||
this.engine.use(i18nBackend).init({
|
this.engine.use(i18nBackend).init({
|
||||||
load: 'languageOnly',
|
load: 'languageOnly',
|
||||||
@ -21,12 +22,12 @@ module.exports = {
|
|||||||
lng: wiki.config.site.lang,
|
lng: wiki.config.site.lang,
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: path.join(wiki.SERVERPATH, 'locales/{{lng}}/{{ns}}.json')
|
loadPath: path.join(wiki.SERVERPATH, 'locales/{{lng}}/{{ns}}.yml')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
getByNamespace(locale, namespace) {
|
async getByNamespace(locale, namespace) {
|
||||||
if (this.engine.hasResourceBundle(locale, namespace)) {
|
if (this.engine.hasResourceBundle(locale, namespace)) {
|
||||||
let data = this.engine.getResourceBundle(locale, namespace)
|
let data = this.engine.getResourceBundle(locale, namespace)
|
||||||
return _.map(dotize.convert(data), (value, key) => {
|
return _.map(dotize.convert(data), (value, key) => {
|
||||||
@ -39,12 +40,12 @@ module.exports = {
|
|||||||
throw new Error('Invalid locale or namespace')
|
throw new Error('Invalid locale or namespace')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loadLocale(locale) {
|
async loadLocale(locale) {
|
||||||
return Promise.fromCallback(cb => {
|
return Promise.fromCallback(cb => {
|
||||||
return this.engine.loadLanguages(locale, cb)
|
return this.engine.loadLanguages(locale, cb)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setCurrentLocale(locale) {
|
async setCurrentLocale(locale) {
|
||||||
return Promise.fromCallback(cb => {
|
return Promise.fromCallback(cb => {
|
||||||
return this.engine.changeLanguage(locale, cb)
|
return this.engine.changeLanguage(locale, cb)
|
||||||
})
|
})
|
||||||
|
@ -19,6 +19,22 @@ module.exports = {
|
|||||||
limit: 1
|
limit: 1
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
login(obj, args, context) {
|
||||||
|
return wiki.db.User.login(args, context).catch(err => {
|
||||||
|
return {
|
||||||
|
succeeded: false,
|
||||||
|
message: err.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loginTFA(obj, args, context) {
|
||||||
|
return wiki.db.User.loginTFA(args, context).catch(err => {
|
||||||
|
return {
|
||||||
|
succeeded: false,
|
||||||
|
message: err.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
modifyUser(obj, args) {
|
modifyUser(obj, args) {
|
||||||
return wiki.db.User.update({
|
return wiki.db.User.update({
|
||||||
email: args.email,
|
email: args.email,
|
||||||
|
@ -148,8 +148,16 @@ type User implements Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OperationResult {
|
type OperationResult {
|
||||||
succeded: Boolean!
|
succeeded: Boolean!
|
||||||
message: String
|
message: String
|
||||||
|
data: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResult {
|
||||||
|
succeeded: Boolean!
|
||||||
|
message: String
|
||||||
|
tfaRequired: Boolean
|
||||||
|
tfaLoginToken: String
|
||||||
}
|
}
|
||||||
|
|
||||||
# Query (Read)
|
# Query (Read)
|
||||||
@ -249,6 +257,17 @@ type Mutation {
|
|||||||
id: Int!
|
id: Int!
|
||||||
): OperationResult
|
): OperationResult
|
||||||
|
|
||||||
|
login(
|
||||||
|
username: String!
|
||||||
|
password: String!
|
||||||
|
provider: String!
|
||||||
|
): LoginResult
|
||||||
|
|
||||||
|
loginTFA(
|
||||||
|
loginToken: String!
|
||||||
|
securityCode: String!
|
||||||
|
): OperationResult
|
||||||
|
|
||||||
modifyComment(
|
modifyComment(
|
||||||
id: Int!
|
id: Int!
|
||||||
content: String!
|
content: String!
|
||||||
|
Loading…
x
Reference in New Issue
Block a user