Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
cb0d8690
Commit
cb0d8690
authored
Jan 09, 2018
by
NGPixel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: login + TFA authentication
parent
85717bd3
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
402 additions
and
44 deletions
+402
-44
login.vue
client/js/components/login.vue
+141
-19
graphql.js
client/js/constants/graphql.js
+18
-0
login.scss
client/scss/components/login.scss
+23
-0
data.yml
server/app/data.yml
+3
-0
local.js
server/extensions/authentication/local.js
+7
-2
error.js
server/helpers/error.js
+30
-0
security.js
server/helpers/security.js
+20
-10
index.js
server/index.js
+1
-0
master.js
server/master.js
+11
-1
user.js
server/models/user.js
+105
-5
auth.js
server/modules/auth.js
+1
-1
localization.js
server/modules/localization.js
+6
-5
resolvers-user.js
server/schemas/resolvers-user.js
+16
-0
types.graphql
server/schemas/types.graphql
+20
-1
No files found.
client/js/components/login.vue
View file @
cb0d8690
<
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:login
r
equired'
)
}}
input(type='text', ref='iptEmail',
:placeholder='$t("auth:fields.emailu
ser")')
input(type='password', ref='iptPassword',
:placeholder='$t("auth:fields.password")
')
h2
{{
$t
(
'auth:login
R
equired'
)
}}
input(type='text', ref='iptEmail',
v-model='username', :placeholder='$t("auth:fields.emailU
ser")')
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.powered
b
y'
)
}}
span
{{
$t
(
'footer.powered
B
y'
)
}}
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
()
...
...
client/js/constants/graphql.js
View file @
cb0d8690
...
...
@@ -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
}
}
`
}
client/scss/components/login.scss
View file @
cb0d8690
...
...
@@ -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
;
...
...
server/app/data.yml
View file @
cb0d8690
...
...
@@ -53,6 +53,9 @@ configNamespaces:
-
site
-
theme
-
uploads
localeNamespaces
:
-
auth
-
common
queues
:
-
gitSync
-
uplClearTemp
...
...
server/extensions/authentication/local.js
View file @
cb0d8690
...
...
@@ -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
)
...
...
server/helpers/error.js
0 → 100644
View file @
cb0d8690
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
}
server/helpers/security.js
View file @
cb0d8690
'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'
)
})
}
}
server/index.js
View file @
cb0d8690
...
...
@@ -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'
)
}
...
...
server/master.js
View file @
cb0d8690
...
...
@@ -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
)
...
...
server/models/user.js
View file @
cb0d8690
/* 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'
&&
(
app
config
.
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
,
...
...
server/modules/auth.js
View file @
cb0d8690
...
...
@@ -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
)
{
...
...
server/modules/localization.js
View file @
cb0d8690
...
...
@@ -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
)
})
...
...
server/schemas/resolvers-user.js
View file @
cb0d8690
...
...
@@ -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
,
...
...
server/schemas/types.graphql
View file @
cb0d8690
...
...
@@ -148,8 +148,16 @@ type User implements Base {
}
type
OperationResult
{
succeded
:
Boolean
!
succe
e
ded
:
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
!
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment