Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
module.exports = do ->
Dependencies
crypto = require 'crypto'
ld = require 'lodash'
cuid = require 'cuid'
storage = require '../storage.js'
common = require './common.js'
UPREFIX = storage.DBPREFIX.USER
CPREFIX = storage.DBPREFIX.CONF
The user
is the masterpiece of the MyPads plugin.
It initially contains :
ids
, an in memory object to map \_id
to login
field and ensures
uniqueness of logins user = ids: {}
These functions are not private like with closures, for testing purposes, but they are expected be used only internally by other MyPads functions.
user.fn = {}
getPasswordConf
is an asynchronous function that get from database values
for minimum and maximum passwords. It takes a callback
function as unique
argument called with Error or null and results.
Internally, it uses storage.getKeys
.
user.fn.getPasswordConf = (callback) ->
_keys = ["#{CPREFIX}passwordMin", "#{CPREFIX}passwordMax"]
storage.fn.getKeys _keys, (err, results) ->
return callback err if err
callback null, results
checkPasswordLength
is a private helper aiming at respecting the minimum
length fixed into MyPads configuration.
It takes two arguments, with fields
password
stringparams
object withpasswordMin
sizepasswordMax
sizeIt returns a TypeError if the verification has failed.
user.fn.checkPasswordLength = (password, params) ->
min = params["#{CPREFIX}passwordMin"]
max = params["#{CPREFIX}passwordMax"]
if password.length < min or password.length > max
return new TypeError "password length must be between #{min} and
#{max} characters"
genPassword
is an asynchronous function which do :
conf.passwordMin
and conf.passwordMax
password
matches the already used one in case of updatesalt
and hashed
password
It takes
old
user object, null in case of creationuser
objectcallback
function returning Error if needed, or null and the
updated user
object user.fn.genPassword = (old, u, callback) ->
user.fn.getPasswordConf (err, res) ->
return callback err if err
err = user.fn.checkPasswordLength u.password, res
return callback err if err
newPass = ->
user.fn.hashPassword null, u.password, (err, pass) ->
return callback err if err
u.password = pass
callback null, u
if old
oldp = old.password
user.fn.hashPassword oldp.salt, u.password, (err, p) ->
return callback err if err
if p.hash is oldp.hash
u.password = oldp
callback null, u
else
newPass()
else
newPass()
hashPassword
is an asynchronous function that use crypto.randomBytes
to
generate a strong salt
if needed and return a sha512
hash
composed of
the salt
and the given password
. It takes
salt
stringpassword
stringcallback
function which returns an object with hash
ed password and
the salt
. user.fn.hashPassword = (salt, password, callback) ->
crypto.randomBytes 40, (ex, buf) ->
return callback ex if ex
salt ?= buf.toString 'hex'
sha512 = crypto.createHash 'sha512'
sha512.update salt
callback null,
salt: salt
hash: sha512.update(password).digest 'hex'
assignProps
takes params
object and assign defaults if needed. It adds a
groups
array field, which will hold model.group
of pads ids. It
returns the user object.
user.fn.assignProps = (params) ->
p = params
u = ld.reduce ['firstname', 'lastname', 'organization'],
(res, v) ->
res[v] = if ld.isString p[v] then p[v] else ''
res
, {}
u.email = if ld.isEmail p.email then p.email else ''
u.groups = []
ld.assign { _id: p._id, login: p.login, password: p.password }, u
This is a function which check if id or login are already taken for new users and if the login has changed for existing users (updates).
It takes, as arguments
id
, from params.\_id
from user.set
u
user objectIt returns, through the callback, an Error if the user or login are already here, null otherwise.
user.fn.checkLogin = (_id, u, callback) ->
if not _id
exists = not ld.isUndefined(user.ids[u.login]) or
ld.includes ld.values(user.ids), u._id
if exists
e = 'user already exists, please choose another login'
return callback new Error e
return callback null
else
u.login has changed for existing user
if ld.isUndefined user.ids[u.login]
key = ld.findKey user.ids, (uid) -> uid is _id
delete user.ids[key]
return callback null
Local getDel
wrapper that uses user.ids
object to ensure uniqueness of
login and _id fields before returning common.getDel
with UPREFIX fixed.
It also handles secondary indexes for model.group elements.
It takes the mandatory login
string as argument and return an error if
login already exists. It also takes a mandatory callback
function.
user.fn.getDel = (del, login, callback) ->
if not ld.isString(login) or ld.isEmpty(login)
throw new TypeError 'login must be a string'
if ld.isUndefined user.ids[login]
return callback new Error 'user not found'
cb = callback
if del
cb = (err, u) ->
delete user.ids[u.login]
if u.groups.length
GPREFIX = storage.DBPREFIX.GROUP
storage.fn.getKeys ld.map(u.groups, (g) -> GPREFIX + g),
(err, groups) ->
return callback err if err
groups = ld.reduce(groups, (memo, g) ->
ld.pull g.users, u._id
ld.pull g.admins, u._id
memo[GPREFIX + g._id] = g
memo
, {})
storage.fn.setKeys groups, (err) ->
return callback err if err
callback null, u
else
callback null, u
common.getDel del, UPREFIX, user.ids[login], cb
set
is a function with real user setting into the database and secondary
index handling. It takes :
u
user objectcallback
function, returning an Error or null and the u
object user.fn.set = (u, callback) ->
storage.db.set UPREFIX + u._id, u, (err) ->
return callback err if err
user.ids[u.login] = u._id
callback null, u
init
is a function that is called once at the initialization of mypads
and loops over all users to map their login to their _id and then
ensures uniqueness.
It takes a callback function which is returned with null when finished.
user.init = (callback) ->
storage.db.findKeys "#{UPREFIX}*", null, (err, keys) ->
return callback err if err
storage.fn.getKeys keys, (err, results) ->
if results
user.ids = ld.transform results, (memo, val, key) ->
memo[val.login] = key.replace UPREFIX, ''
callback null
Creation and update sets the defaults and checks if required fields have been fixed. It takes
params
object, with\_id
string, for update only and existing userlogin
stringpassword
string, between conf.passwordMin and
conf.passwordMax, will be an object with hash
and salt
stringsemail
string, used for communicationfirstname
stringlastname
stringoptional organization
string
a classic callback
function returning Error if error, null otherwise
and the user object
It takes care of updating correcly the user.ids
in-memory index.
groups
array can't be fixed here but will be retrieved from database in
case of update.
user.set = (params, callback) ->
common.addSetInit params, callback, ['login', 'password']
u = user.fn.assignProps params
u._id ?= cuid()
user.fn.checkLogin params._id, u, (err) ->
return callback err if err
Update/Edit case
if params._id
user.get u.login, (err, dbuser) ->
return callback err if err
u.groups = dbuser.groups
user.fn.genPassword dbuser, u, (err, u) ->
return callback err if err
user.fn.set u, callback
else
user.fn.genPassword null, u, (err, u) ->
return callback err if err
user.fn.set u, callback
User reading
This function uses user.fn.getDel
and common.getDel
with del
to
false . It takes mandatory login
string and callback
function.
user.get = ld.partial user.fn.getDel, false
User removal
This function uses user.fn.getDel
and common.getDel
with del
to
true . It takes mandatory login
string and callback
function.
user.del = ld.partial user.fn.getDel, true
Here are lodash user extensions for MyPads.
isEmail
checks if given string is an email or not. It takes a value and
returns a boolean.
ld.mixin isEmail: (val) ->
rg = new RegExp ['[a-z0-9!#$%&\'*+/=?^_`{|}~-]+',
'(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9]',
'(?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9]',
'(?:[a-z0-9-]*[a-z0-9])?'].join ''
ld.isString(val) and rg.test(val)
return user