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
78ae137f
Commit
78ae137f
authored
Dec 20, 2018
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: register server-side validation + forgot password UI
parent
901dbb98
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
140 additions
and
29 deletions
+140
-29
login.vue
client/components/login.vue
+44
-6
register.vue
client/components/register.vue
+11
-9
auth.js
server/controllers/auth.js
+7
-2
error.js
server/helpers/error.js
+12
-0
authentication.js
server/models/authentication.js
+4
-0
users.js
server/models/users.js
+62
-12
No files found.
client/components/login.vue
View file @
78ae137f
...
@@ -45,7 +45,7 @@
...
@@ -45,7 +45,7 @@
:
placeholder
=
'$t("auth:fields.password")'
:
placeholder
=
'$t("auth:fields.password")'
@
keyup
.
enter
=
'login'
@
keyup
.
enter
=
'login'
)
)
template
(
v
-
if
=
'screen === "tfa"'
)
template
(
v
-
else
-
if
=
'screen === "tfa"'
)
.
body
-
2
Enter
the
security
code
generated
from
your
trusted
device
:
.
body
-
2
Enter
the
security
code
generated
from
your
trusted
device
:
v
-
text
-
field
.
md2
.
centered
.
mt
-
2
(
v
-
text
-
field
.
md2
.
centered
.
mt
-
2
(
solo
solo
...
@@ -57,6 +57,18 @@
...
@@ -57,6 +57,18 @@
:
placeholder
=
'$t("auth:tfa.placeholder")'
:
placeholder
=
'$t("auth:tfa.placeholder")'
@
keyup
.
enter
=
'verifySecurityCode'
@
keyup
.
enter
=
'verifySecurityCode'
)
)
template
(
v
-
else
-
if
=
'screen === "forgot"'
)
.
body
-
2
{{
$t
(
'auth:forgotPasswordSubtitle'
)
}}
v
-
text
-
field
.
md2
.
mt
-
3
(
solo
flat
prepend
-
icon
=
'email'
background
-
color
=
'grey lighten-4'
hide
-
details
ref
=
'iptEmailForgot'
v
-
model
=
'username'
:
placeholder
=
'$t("auth:fields.email")'
)
v
-
card
-
actions
.
pb
-
4
v
-
card
-
actions
.
pb
-
4
v
-
spacer
v
-
spacer
v
-
btn
.
md2
(
v
-
btn
.
md2
(
...
@@ -69,7 +81,7 @@
...
@@ -69,7 +81,7 @@
:
loading
=
'isLoading'
:
loading
=
'isLoading'
)
{{
$t
(
'auth:actions.login'
)
}}
)
{{
$t
(
'auth:actions.login'
)
}}
v
-
btn
.
md2
(
v
-
btn
.
md2
(
v
-
if
=
'screen === "tfa"'
v
-
else
-
if
=
'screen === "tfa"'
block
block
large
large
color
=
'primary'
color
=
'primary'
...
@@ -77,12 +89,25 @@
...
@@ -77,12 +89,25 @@
round
round
:
loading
=
'isLoading'
:
loading
=
'isLoading'
)
{{
$t
(
'auth:tfa.verifyToken'
)
}}
)
{{
$t
(
'auth:tfa.verifyToken'
)
}}
v
-
btn
.
md2
(
v
-
else
-
if
=
'screen === "forgot"'
block
large
color
=
'primary'
@
click
=
'forgotPasswordSubmit'
round
:
loading
=
'isLoading'
)
{{
$t
(
'auth:sendResetPassword'
)
}}
v
-
spacer
v
-
spacer
v
-
card
-
actions
.
pb
-
3
(
v
-
if
=
'selectedStrategy.key === "local"'
)
v
-
card
-
actions
.
pb
-
3
(
v
-
if
=
's
creen === "login" && s
electedStrategy.key === "local"'
)
v
-
spacer
v
-
spacer
a
.
caption
(
href
=
'
'
)
{{
$t
(
'auth:forgotPasswordLink'
)
}}
a
.
caption
(
@
click
.
stop
.
prevent
=
'forgotPassword'
,
href
=
'#forgot
'
)
{{
$t
(
'auth:forgotPasswordLink'
)
}}
v
-
spacer
v
-
spacer
template
(
v
-
if
=
'isSocialShown'
)
v
-
card
-
actions
.
pb
-
3
(
v
-
else
-
if
=
'screen === "forgot"'
)
v
-
spacer
a
.
caption
(@
click
.
stop
.
prevent
=
'screen = `login`'
,
href
=
'#cancelforgot'
)
{{
$t
(
'auth:forgotPasswordCancel'
)
}}
v
-
spacer
template
(
v
-
if
=
'screen === "login" && isSocialShown'
)
v
-
divider
v
-
divider
v
-
card
-
text
.
grey
.
lighten
-
4
.
text
-
xs
-
center
v
-
card
-
text
.
grey
.
lighten
-
4
.
text
-
xs
-
center
.
pb
-
2
.
body
-
2
.
text
-
xs
-
center
.
grey
--
text
.
text
--
darken
-
2
{{
$t
(
'auth:orLoginUsingStrategy'
)
}}
.
pb
-
2
.
body
-
2
.
text
-
xs
-
center
.
grey
--
text
.
text
--
darken
-
2
{{
$t
(
'auth:orLoginUsingStrategy'
)
}}
...
@@ -95,7 +120,7 @@
...
@@ -95,7 +120,7 @@
@
click
=
'selectStrategy(strategy)'
@
click
=
'selectStrategy(strategy)'
)
)
span
{{
strategy
.
title
}}
span
{{
strategy
.
title
}}
template
(
v
-
if
=
'selectedStrategy.selfRegistration'
)
template
(
v
-
if
=
's
creen === "login" && s
electedStrategy.selfRegistration'
)
v
-
divider
v
-
divider
v
-
card
-
actions
.
py
-
3
(:
class
=
'isSocialShown ? "" : "grey lighten-4"'
)
v
-
card
-
actions
.
py
-
3
(:
class
=
'isSocialShown ? "" : "grey lighten-4"'
)
v
-
spacer
v
-
spacer
...
@@ -286,6 +311,19 @@ export default {
...
@@ -286,6 +311,19 @@ export default {
this
.
isLoading
=
false
this
.
isLoading
=
false
}
)
}
)
}
}
}
,
forgotPassword
()
{
this
.
screen
=
'forgot'
this
.
$nextTick
(()
=>
{
this
.
$refs
.
iptEmailForgot
.
focus
()
}
)
}
,
async
forgotPasswordSubmit
()
{
this
.
$store
.
commit
(
'showNotification'
,
{
style
:
'pink'
,
message
:
'Coming soon!'
,
icon
:
'free_breakfast'
}
)
}
}
}
,
}
,
apollo
:
{
apollo
:
{
...
...
client/components/register.vue
View file @
78ae137f
...
@@ -10,7 +10,7 @@
...
@@ -10,7 +10,7 @@
offset-lg3, lg6
offset-lg3, lg6
offset-xl4, xl4
offset-xl4, xl4
)
)
transition(name='
zoom
')
transition(name='
fadeUp
')
v-card.elevation-5.md2(v-show='isShown')
v-card.elevation-5.md2(v-show='isShown')
v-toolbar(color='indigo', flat, dense, dark)
v-toolbar(color='indigo', flat, dense, dark)
v-spacer
v-spacer
...
@@ -43,6 +43,7 @@
...
@@ -43,6 +43,7 @@
:placeholder='$t("auth:fields.password")'
:placeholder='$t("auth:fields.password")'
color='indigo'
color='indigo'
loading
loading
counter='255'
)
)
password-strength(slot='progress', v-model='password')
password-strength(slot='progress', v-model='password')
v-text-field.md2.mt-2(
v-text-field.md2.mt-2(
...
@@ -63,12 +64,12 @@
...
@@ -63,12 +64,12 @@
flat
flat
prepend-icon='person'
prepend-icon='person'
background-color='grey lighten-4'
background-color='grey lighten-4'
hide-details
ref='iptName'
ref='iptName'
v-model='name'
v-model='name'
:placeholder='$t("auth:fields.name")'
:placeholder='$t("auth:fields.name")'
@keyup.enter='register'
@keyup.enter='register'
color='indigo'
color='indigo'
counter='255'
)
)
v-card-actions.pb-4
v-card-actions.pb-4
v-spacer
v-spacer
...
@@ -116,7 +117,9 @@ export default {
...
@@ -116,7 +117,9 @@ export default {
name
:
''
,
name
:
''
,
hidePassword
:
true
,
hidePassword
:
true
,
isLoading
:
false
,
isLoading
:
false
,
isShown
:
false
isShown
:
false
,
loaderColor
:
'grey darken-4'
,
loaderTitle
:
'Working...'
}
}
},
},
computed
:
{
computed
:
{
...
@@ -211,6 +214,8 @@ export default {
...
@@ -211,6 +214,8 @@ export default {
this
.
$refs
.
iptName
.
focus
()
this
.
$refs
.
iptName
.
focus
()
}
}
}
else
{
}
else
{
this
.
loaderColor
=
'grey darken-4'
this
.
loaderTitle
=
this
.
$t
(
'auth:registering'
)
this
.
isLoading
=
true
this
.
isLoading
=
true
try
{
try
{
let
resp
=
await
this
.
$apollo
.
mutate
({
let
resp
=
await
this
.
$apollo
.
mutate
({
...
@@ -224,11 +229,8 @@ export default {
...
@@ -224,11 +229,8 @@ export default {
if
(
_
.
has
(
resp
,
'data.authentication.register'
))
{
if
(
_
.
has
(
resp
,
'data.authentication.register'
))
{
let
respObj
=
_
.
get
(
resp
,
'data.authentication.register'
,
{})
let
respObj
=
_
.
get
(
resp
,
'data.authentication.register'
,
{})
if
(
respObj
.
responseResult
.
succeeded
===
true
)
{
if
(
respObj
.
responseResult
.
succeeded
===
true
)
{
this
.
$store
.
commit
(
'showNotification'
,
{
this
.
loaderColor
=
'green'
message
:
'Account created successfully! Redirecting...'
,
this
.
loaderTitle
=
this
.
$t
(
'auth:registerSuccess'
)
style
:
'success'
,
icon
:
'check'
})
Cookies
.
set
(
'jwt'
,
respObj
.
jwt
,
{
expires
:
365
})
Cookies
.
set
(
'jwt'
,
respObj
.
jwt
,
{
expires
:
365
})
_
.
delay
(()
=>
{
_
.
delay
(()
=>
{
window
.
location
.
replace
(
'/'
)
window
.
location
.
replace
(
'/'
)
...
@@ -237,7 +239,7 @@ export default {
...
@@ -237,7 +239,7 @@ export default {
throw
new
Error
(
respObj
.
responseResult
.
message
)
throw
new
Error
(
respObj
.
responseResult
.
message
)
}
}
}
else
{
}
else
{
throw
new
Error
(
'Registration is unavailable at this time.'
)
throw
new
Error
(
this
.
$t
(
'auth:genericError'
)
)
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
console
.
error
(
err
)
console
.
error
(
err
)
...
...
server/controllers/auth.js
View file @
78ae137f
...
@@ -21,8 +21,13 @@ router.get('/logout', function (req, res) {
...
@@ -21,8 +21,13 @@ router.get('/logout', function (req, res) {
/**
/**
* Register form
* Register form
*/
*/
router
.
get
(
'/register'
,
function
(
req
,
res
,
next
)
{
router
.
get
(
'/register'
,
async
(
req
,
res
,
next
)
=>
{
res
.
render
(
'register'
)
const
localStrg
=
await
WIKI
.
models
.
authentication
.
getStrategy
(
'local'
)
if
(
localStrg
.
selfRegistration
)
{
res
.
render
(
'register'
)
}
else
{
next
(
new
WIKI
.
Error
.
AuthRegistrationDisabled
())
}
})
})
/**
/**
...
...
server/helpers/error.js
View file @
78ae137f
...
@@ -17,6 +17,14 @@ module.exports = {
...
@@ -17,6 +17,14 @@ module.exports = {
message
:
'An account already exists using this email address.'
,
message
:
'An account already exists using this email address.'
,
code
:
1004
code
:
1004
}),
}),
AuthRegistrationDisabled
:
CustomError
(
'AuthRegistrationDisabled'
,
{
message
:
'Registration is disabled. Contact your system administrator.'
,
code
:
1011
}),
AuthRegistrationDomainUnauthorized
:
CustomError
(
'AuthRegistrationDomainUnauthorized'
,
{
message
:
'You are not authorized to register. Must use a whitelisted domain.'
,
code
:
1012
}),
AuthTFAFailed
:
CustomError
(
'AuthTFAFailed'
,
{
AuthTFAFailed
:
CustomError
(
'AuthTFAFailed'
,
{
message
:
'Incorrect TFA Security Code.'
,
message
:
'Incorrect TFA Security Code.'
,
code
:
1005
code
:
1005
...
@@ -33,6 +41,10 @@ module.exports = {
...
@@ -33,6 +41,10 @@ module.exports = {
message
:
'Too many attempts! Try again later.'
,
message
:
'Too many attempts! Try again later.'
,
code
:
1008
code
:
1008
}),
}),
InputInvalid
:
CustomError
(
'InputInvalid'
,
{
message
:
'Input data is invalid.'
,
code
:
1013
}),
LocaleInvalidNamespace
:
CustomError
(
'LocaleInvalidNamespace'
,
{
LocaleInvalidNamespace
:
CustomError
(
'LocaleInvalidNamespace'
,
{
message
:
'Invalid locale or namespace.'
,
message
:
'Invalid locale or namespace.'
,
code
:
1009
code
:
1009
...
...
server/models/authentication.js
View file @
78ae137f
...
@@ -30,6 +30,10 @@ module.exports = class Authentication extends Model {
...
@@ -30,6 +30,10 @@ module.exports = class Authentication extends Model {
}
}
}
}
static
async
getStrategy
(
key
)
{
return
WIKI
.
models
.
authentication
.
query
().
findOne
({
key
})
}
static
async
getStrategies
(
isEnabled
)
{
static
async
getStrategies
(
isEnabled
)
{
const
strategies
=
await
WIKI
.
models
.
authentication
.
query
().
where
(
_
.
isBoolean
(
isEnabled
)
?
{
isEnabled
}
:
{})
const
strategies
=
await
WIKI
.
models
.
authentication
.
query
().
where
(
_
.
isBoolean
(
isEnabled
)
?
{
isEnabled
}
:
{})
return
_
.
sortBy
(
strategies
.
map
(
str
=>
({
return
_
.
sortBy
(
strategies
.
map
(
str
=>
({
...
...
server/models/users.js
View file @
78ae137f
...
@@ -6,6 +6,7 @@ const tfa = require('node-2fa')
...
@@ -6,6 +6,7 @@ const tfa = require('node-2fa')
const
securityHelper
=
require
(
'../helpers/security'
)
const
securityHelper
=
require
(
'../helpers/security'
)
const
jwt
=
require
(
'jsonwebtoken'
)
const
jwt
=
require
(
'jsonwebtoken'
)
const
Model
=
require
(
'objection'
).
Model
const
Model
=
require
(
'objection'
).
Model
const
validate
=
require
(
'validate.js'
)
const
bcryptRegexp
=
/^
\$
2
[
ayb
]\$[
0-9
]{2}\$[
A-Za-z0-9.
/]{53}
$/
const
bcryptRegexp
=
/^
\$
2
[
ayb
]\$[
0-9
]{2}\$[
A-Za-z0-9.
/]{53}
$/
...
@@ -294,21 +295,70 @@ module.exports = class User extends Model {
...
@@ -294,21 +295,70 @@ module.exports = class User extends Model {
}
}
static
async
register
({
email
,
password
,
name
},
context
)
{
static
async
register
({
email
,
password
,
name
},
context
)
{
const
usr
=
await
WIKI
.
models
.
users
.
query
().
findOne
({
email
,
providerKey
:
'local'
})
const
localStrg
=
await
WIKI
.
models
.
authentication
.
getStrategy
(
'local'
)
if
(
!
usr
)
{
// Check if self-registration is enabled
await
WIKI
.
models
.
users
.
query
().
insert
({
if
(
localStrg
.
selfRegistration
)
{
provider
:
'local'
,
// Input validation
const
validation
=
validate
({
email
,
email
,
name
,
password
,
password
,
locale
:
'en'
,
name
defaultEditor
:
'markdown'
,
},
{
tfaIsActive
:
false
,
email
:
{
isSystem
:
false
email
:
true
,
})
length
:
{
return
true
maximum
:
255
}
},
password
:
{
presence
:
{
allowEmpty
:
false
},
length
:
{
minimum
:
6
}
},
name
:
{
presence
:
{
allowEmpty
:
false
},
length
:
{
minimum
:
2
,
maximum
:
255
}
},
},
{
format
:
'flat'
})
if
(
validation
&&
validation
.
length
>
0
)
{
throw
new
WIKI
.
Error
.
InputInvalid
(
validation
[
0
])
}
// Check if email domain is whitelisted
if
(
_
.
get
(
localStrg
,
'domainWhitelist.v'
,
[]).
length
>
0
)
{
const
emailDomain
=
_
.
last
(
email
.
split
(
'@'
))
if
(
!
_
.
includes
(
localStrg
.
domainWhitelist
.
v
,
emailDomain
))
{
throw
new
WIKI
.
Error
.
AuthRegistrationDomainUnauthorized
()
}
}
// Check if email already exists
const
usr
=
await
WIKI
.
models
.
users
.
query
().
findOne
({
email
,
providerKey
:
'local'
})
if
(
!
usr
)
{
// Create the account
await
WIKI
.
models
.
users
.
query
().
insert
({
provider
:
'local'
,
email
,
name
,
password
,
locale
:
'en'
,
defaultEditor
:
'markdown'
,
tfaIsActive
:
false
,
isSystem
:
false
})
return
true
}
else
{
throw
new
WIKI
.
Error
.
AuthAccountAlreadyExists
()
}
}
else
{
}
else
{
throw
new
WIKI
.
Error
.
Auth
AccountAlreadyExists
()
throw
new
WIKI
.
Error
.
Auth
RegistrationDisabled
()
}
}
}
}
}
}
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