feat: login + TFA authentication
This commit is contained in:
parent
85717bd369
commit
cb0d86906f
@ -1,20 +1,29 @@
|
||||
<template lang="pug">
|
||||
.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')
|
||||
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')
|
||||
span {{ strategy.title }}
|
||||
.login-providers-fill
|
||||
.login-frame
|
||||
.login-frame(v-show='screen === "login"')
|
||||
h1 {{ siteTitle }}
|
||||
h2 {{ $t('auth:loginrequired') }}
|
||||
input(type='text', ref='iptEmail', :placeholder='$t("auth:fields.emailuser")')
|
||||
input(type='password', ref='iptPassword', :placeholder='$t("auth:fields.password")')
|
||||
h2 {{ $t('auth:loginRequired') }}
|
||||
input(type='text', ref='iptEmail', v-model='username', :placeholder='$t("auth:fields.emailUser")')
|
||||
input(type='password', ref='iptPassword', v-model='password', :placeholder='$t("auth:fields.password")', @keyup.enter='login')
|
||||
button.button.is-blue.is-fullwidth(@click='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
|
||||
span {{ $t('footer.poweredby') }}
|
||||
span {{ $t('footer.poweredBy') }}
|
||||
a(href='https://wiki.js.org', rel='external', title='Wiki.js') Wiki.js
|
||||
</template>
|
||||
|
||||
@ -23,26 +32,36 @@
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
data() {
|
||||
data () {
|
||||
return {
|
||||
error: false,
|
||||
strategies: [],
|
||||
selectedStrategy: 'local'
|
||||
selectedStrategy: 'local',
|
||||
screen: 'login',
|
||||
username: '',
|
||||
password: '',
|
||||
securityCode: '',
|
||||
loginToken: '',
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
siteTitle() {
|
||||
siteTitle () {
|
||||
return siteConfig.title
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectStrategy(key, useForm) {
|
||||
selectStrategy (key, useForm) {
|
||||
this.selectedStrategy = key
|
||||
this.screen = 'login'
|
||||
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({
|
||||
query: CONSTANTS.GRAPHQL.GQL_QUERY_AUTHENTICATION,
|
||||
variables: {
|
||||
@ -54,19 +73,122 @@ export default {
|
||||
} else {
|
||||
throw new Error('No authentication providers available!')
|
||||
}
|
||||
this.isLoading = false
|
||||
}).catch(err => {
|
||||
console.error(err)
|
||||
this.$store.dispatch('alert', {
|
||||
style: 'error',
|
||||
icon: 'gg-warning',
|
||||
msg: err.message
|
||||
})
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
login() {
|
||||
this.$store.dispatch('alert', {
|
||||
style: 'error',
|
||||
icon: 'gg-warning',
|
||||
msg: 'Email or password is invalid'
|
||||
})
|
||||
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 () {
|
||||
this.$store.commit('navigator/subtitleStatic', 'Login')
|
||||
this.refreshStrategies()
|
||||
this.$refs.iptEmail.focus()
|
||||
|
@ -18,5 +18,23 @@ export default {
|
||||
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;
|
||||
animation: zoomIn .5s ease;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
content: " ";
|
||||
@include spinner(mc('blue', '500'),0.5s,16px);
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.is-expanded {
|
||||
width: 650px;
|
||||
|
||||
@ -67,6 +76,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.is-loading::after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@include until($tablet) {
|
||||
width: 100%;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -53,6 +53,9 @@ configNamespaces:
|
||||
- site
|
||||
- theme
|
||||
- uploads
|
||||
localeNamespaces:
|
||||
- auth
|
||||
- common
|
||||
queues:
|
||||
- gitSync
|
||||
- uplClearTemp
|
||||
|
@ -17,7 +17,12 @@ module.exports = {
|
||||
usernameField: 'email',
|
||||
passwordField: 'password'
|
||||
}, (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) {
|
||||
return user.validatePassword(uPassword).then(() => {
|
||||
return done(null, user) || true
|
||||
@ -25,7 +30,7 @@ module.exports = {
|
||||
return done(err, null)
|
||||
})
|
||||
} else {
|
||||
return done(new Error('INVALID_LOGIN'), null)
|
||||
return done(new wiki.Error.AuthLoginFailed(), null)
|
||||
}
|
||||
}).catch((err) => {
|
||||
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'
|
||||
|
||||
/* global appdata, appconfig */
|
||||
|
||||
const _ = require('lodash')
|
||||
const Promise = require('bluebird')
|
||||
const crypto = require('crypto')
|
||||
|
||||
module.exports = {
|
||||
sanitizeCommitUser (user) {
|
||||
let wlist = new RegExp('[^a-zA-Z0-9-_.\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g')
|
||||
return {
|
||||
name: _.chain(user.name).replace(wlist, '').trim().value(),
|
||||
email: appconfig.git.showUserEmail ? user.email : appconfig.git.serverEmail
|
||||
}
|
||||
// let wlist = new RegExp('[^a-zA-Z0-9-_.\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g')
|
||||
// return {
|
||||
// name: _.chain(user.name).replace(wlist, '').trim().value(),
|
||||
// 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,
|
||||
ROOTPATH: process.cwd(),
|
||||
SERVERPATH: path.join(process.cwd(), 'server'),
|
||||
Error: require('./helpers/error'),
|
||||
configSvc: require('./modules/config'),
|
||||
kernel: require('./modules/kernel')
|
||||
}
|
||||
|
@ -114,7 +114,17 @@ module.exports = async () => {
|
||||
|
||||
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('/uploads', mw.auth, ctrl.uploads)
|
||||
app.use('/admin', mw.auth, ctrl.admin)
|
||||
|
@ -1,8 +1,10 @@
|
||||
/* global wiki, appconfig */
|
||||
/* global wiki */
|
||||
|
||||
const Promise = require('bluebird')
|
||||
const bcrypt = require('bcryptjs-then')
|
||||
const _ = require('lodash')
|
||||
const tfa = require('node-2fa')
|
||||
const securityHelper = require('../helpers/security')
|
||||
|
||||
/**
|
||||
* Users schema
|
||||
@ -56,10 +58,108 @@ module.exports = (sequelize, DataTypes) => {
|
||||
]
|
||||
})
|
||||
|
||||
userSchema.prototype.validatePassword = function (rawPwd) {
|
||||
return bcrypt.compare(rawPwd, this.password).then((isValid) => {
|
||||
return (isValid) ? true : Promise.reject(new Error(wiki.lang.t('auth:errors:invalidlogin')))
|
||||
userSchema.prototype.validatePassword = async function (rawPwd) {
|
||||
if (await bcrypt.compare(rawPwd, this.password) === true) {
|
||||
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) => {
|
||||
@ -92,7 +192,7 @@ module.exports = (sequelize, DataTypes) => {
|
||||
new: true
|
||||
}).then((user) => {
|
||||
// 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 = {
|
||||
email: primaryEmail,
|
||||
provider: profile.provider,
|
||||
|
@ -13,7 +13,7 @@ module.exports = {
|
||||
// Serialization user methods
|
||||
|
||||
passport.serializeUser(function (user, done) {
|
||||
done(null, user._id)
|
||||
done(null, user.id)
|
||||
})
|
||||
|
||||
passport.deserializeUser(function (id, done) {
|
||||
|
@ -9,8 +9,9 @@ const Promise = require('bluebird')
|
||||
|
||||
module.exports = {
|
||||
engine: null,
|
||||
namespaces: ['common', 'admin', 'auth', 'errors', 'git'],
|
||||
namespaces: [],
|
||||
init() {
|
||||
this.namespaces = wiki.data.localeNamespaces
|
||||
this.engine = i18next
|
||||
this.engine.use(i18nBackend).init({
|
||||
load: 'languageOnly',
|
||||
@ -21,12 +22,12 @@ module.exports = {
|
||||
lng: wiki.config.site.lang,
|
||||
fallbackLng: 'en',
|
||||
backend: {
|
||||
loadPath: path.join(wiki.SERVERPATH, 'locales/{{lng}}/{{ns}}.json')
|
||||
loadPath: path.join(wiki.SERVERPATH, 'locales/{{lng}}/{{ns}}.yml')
|
||||
}
|
||||
})
|
||||
return this
|
||||
},
|
||||
getByNamespace(locale, namespace) {
|
||||
async getByNamespace(locale, namespace) {
|
||||
if (this.engine.hasResourceBundle(locale, namespace)) {
|
||||
let data = this.engine.getResourceBundle(locale, namespace)
|
||||
return _.map(dotize.convert(data), (value, key) => {
|
||||
@ -39,12 +40,12 @@ module.exports = {
|
||||
throw new Error('Invalid locale or namespace')
|
||||
}
|
||||
},
|
||||
loadLocale(locale) {
|
||||
async loadLocale(locale) {
|
||||
return Promise.fromCallback(cb => {
|
||||
return this.engine.loadLanguages(locale, cb)
|
||||
})
|
||||
},
|
||||
setCurrentLocale(locale) {
|
||||
async setCurrentLocale(locale) {
|
||||
return Promise.fromCallback(cb => {
|
||||
return this.engine.changeLanguage(locale, cb)
|
||||
})
|
||||
|
@ -19,6 +19,22 @@ module.exports = {
|
||||
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) {
|
||||
return wiki.db.User.update({
|
||||
email: args.email,
|
||||
|
@ -148,8 +148,16 @@ type User implements Base {
|
||||
}
|
||||
|
||||
type OperationResult {
|
||||
succeded: Boolean!
|
||||
succeeded: Boolean!
|
||||
message: String
|
||||
data: String
|
||||
}
|
||||
|
||||
type LoginResult {
|
||||
succeeded: Boolean!
|
||||
message: String
|
||||
tfaRequired: Boolean
|
||||
tfaLoginToken: String
|
||||
}
|
||||
|
||||
# Query (Read)
|
||||
@ -249,6 +257,17 @@ type Mutation {
|
||||
id: Int!
|
||||
): OperationResult
|
||||
|
||||
login(
|
||||
username: String!
|
||||
password: String!
|
||||
provider: String!
|
||||
): LoginResult
|
||||
|
||||
loginTFA(
|
||||
loginToken: String!
|
||||
securityCode: String!
|
||||
): OperationResult
|
||||
|
||||
modifyComment(
|
||||
id: Int!
|
||||
content: String!
|
||||
|
Loading…
Reference in New Issue
Block a user