User Model

License

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

Description

The user is the masterpiece of the MyPads plugin.

It initially contains :

  user = ids: {}

Internal Functions

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

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

checkPasswordLength is a private helper aiming at respecting the minimum length fixed into MyPads configuration.

It takes two arguments, with fields

It 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

genPassword is an asynchronous function which do :

It takes

  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

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

  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

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

checkLogin

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

It 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

getDel

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

set is a function with real user setting into the database and secondary index handling. It takes :

  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

Public Functions

init

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

set

Creation and update sets the defaults and checks if required fields have been fixed. It takes

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

get

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

del

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

lodash mixins

Here are lodash user extensions for MyPads.

isEmail

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