# All files / src/api users.coffee

20.98% Statements 30/143
0% Branches 0/21
0% Functions 0/16
21.13% Lines 30/142

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    3601x
    1x
    1x
    1x

    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x

    1x

    1x

    1x

    1x

    1x

    1x

    1x

    1x

    1x

    1x

    1x

    1x

    1x

    1x

     User = require('../model/users').User
    Q = require 'q'
    logger = require 'winston'
    authorisation = require './authorisation'

    moment = require 'moment'
    randtoken = require 'rand-token'
    contact = require '../contact'
    config = require "../config/config"
    config.newUserExpiry = config.get('newUserExpiry')
    config.userPasswordResetExpiry = config.get('userPasswordResetExpiry')
    config.alerts = config.get('alerts')
    utils = require "../utils"
    atna = require 'atna-audit'
    os = require 'os'
    auditing = require '../auditing'
    himSourceID = config.get('auditing').auditEvents.auditSourceID

    ###
    # Get authentication details
    ###
    exports.authenticate = (email) ->
      email = unescape email

      try
        user = yield User.findOne(email: email).exec()

        if not user
          utils.logAndSetResponse this, 404, "Could not find user by email #{email}", 'info'
          # Audit unknown user requested
          audit = atna.userLoginAudit atna.OUTCOME_SERIOUS_FAILURE, himSourceID, os.hostname(), email
          audit = atna.wrapInSyslog audit
          auditing.sendAuditEvent audit, -> logger.debug 'Processed internal audit'
        else
          this.body =
            salt: user.passwordSalt
            ts: new Date()

      catch e
        utils.logAndSetResponse this, 500, "Error during authentication #{e}", 'error'

    #################################
    ### Reset password Functions ###
    #################################

    passwordResetPlainMessageTemplate = (firstname, setPasswordLink) -> """
    <---------- Existing User - Reset Password ---------->
    Hi #{firstname},

    A request has been made to reset your password on the OpenHIM instance running on #{config.alerts.himInstance}
    Follow the below link to reset your password and log into OpenHIM Console
    #{setPasswordLink}
    <---------- Existing User - Reset Password ---------->
    """

    passwordResetHtmlMessageTemplate = (firstname, setPasswordLink) -> """
    <h1>Reset OpenHIM Password</h1>
    <p>Hi #{firstname},<br/><br/>A request has been made to reset your password on the OpenHIM instance running on #{config.alerts.himInstance}</p>
    <p>Follow the below link to set your password and log into OpenHIM Console</p>
    <p>#{setPasswordLink}</p>
    """

    exports.generateRandomToken = () ->
      return randtoken.generate 32

    ###
    # update user token/expiry and send new password email
    ###
    exports.userPasswordResetRequest = (email) ->
      email = unescape email

      if email is 'root@openhim.org'
        this.body = "Cannot request password reset for 'root@openhim.org'"
        this.status = 403
        return

      # Generate the new user token here
      # set expiry date = true

      token = exports.generateRandomToken()
      duration = config.userPasswordResetExpiry.duration
      durationType = config.userPasswordResetExpiry.durationType
      expiry = moment().add(duration, durationType).utc().format()

      updateUserTokenExpiry =
        token: token
        tokenType: 'existingUser'
        expiry: expiry

      try
        user = yield User.findOneAndUpdate(email: email, updateUserTokenExpiry).exec()

        if not user
          this.body = "Tried to request password reset for invalid email address: #{email}"
          this.status = 404
          logger.info "Tried to request password reset for invalid email address: #{email}"
          return

        consoleURL = config.alerts.consoleURL
        setPasswordLink = "#{consoleURL}/#/set-password/#{token}"

        # Send email to user to reset password
        plainMessage = passwordResetPlainMessageTemplate user.firstname, setPasswordLink
        htmlMessage = passwordResetHtmlMessageTemplate user.firstname, setPasswordLink

        sendEmail = Q.denodeify contact.contactUser
        sendEmailError = yield sendEmail 'email', email, 'OpenHIM Console Password Reset', plainMessage, htmlMessage
        if sendEmailError
          utils.logAndSetResponse this, 500, "Could not send email to user via the API #{e}", 'error'

        logger.info 'The email has been sent to the user'
        this.body = "Successfully set user token/expiry for password reset."
        this.status = 201
        logger.info "User updated token/expiry for password reset #{email}"

      catch e
        utils.logAndSetResponse this, 500, "Could not update user with email #{email} via the API #{e}", 'error'

    #######################################
    ### New User Set Password Functions ###
    #######################################

    # get the new user details
    exports.getUserByToken = (token) ->
      token = unescape token

      try
        projectionRestriction = "email": 1, "firstname": 1, "surname": 1, "msisdn": 1, "token": 1, "tokenType": 1, "locked": 1, "expiry": 1, "_id": 0

        result = yield User.findOne(token: token, projectionRestriction).exec()
        if not result
          this.body = "User with token #{token} could not be found."
          this.status = 404
        else
          # if expiry date has past
          if moment(result.expiry).isBefore(moment())
            # user- set password - expired
            this.body = "Token #{token} has expired"
            this.status = 410
          else
            this.body = result
      catch e
        utils.logAndSetResponse this, 500, "Could not find user with token #{token} via the API #{e}", 'error'

    # update the password/details for the new user
    exports.updateUserByToken = (token) ->
      token = unescape token
      userData = this.request.body

      try
        # first try get new user details to check expiry date
        userDataExpiry = yield User.findOne(token: token).exec()

        if not userDataExpiry
          this.body = "User with token #{token} could not be found."
          this.status = 404
          return
        else
          # if expiry date has past
          if moment(userDataExpiry.expiry).isBefore(moment())
            # new user- set password - expired
            this.body = "User with token #{token} has expired to set their password."
            this.status = 410
            return

      catch e
        utils.logAndSetResponse this, 500, "Could not find user with token #{token} via the API #{e}", 'error'
        return

      # check to make sure 'msisdn' isnt 'undefined' when saving
      if userData.msisdn then msisdn = userData.msisdn else msisdn = null

      # construct user object to prevent other properties from being updated
      userUpdateObj =
        token: null
        tokenType: null
        expiry: null
        passwordAlgorithm: userData.passwordAlgorithm
        passwordSalt: userData.passwordSalt
        passwordHash: userData.passwordHash

      if userDataExpiry.tokenType is 'newUser'
        userUpdateObj.firstname = userData.firstname
        userUpdateObj.surname = userData.surname
        userUpdateObj.locked = false
        userUpdateObj.msisdn = msisdn

      try
        yield User.findOneAndUpdate(token: token, userUpdateObj).exec()
        this.body = "Successfully set new user password."
        logger.info "User updated by token #{token}"
      catch e
        utils.logAndSetResponse this, 500, "Could not update user with token #{token} via the API #{e}", 'error'

    #######################################
    ### New User Set Password Functions ###
    #######################################

    plainMessageTemplate = (firstname, setPasswordLink) -> """
    <---------- New User - Set Password ---------->
    Hi #{firstname},

    A profile has been created for you on the OpenHIM instance running on #{config.alerts.himInstance}
    Follow the below link to set your password and log into OpenHIM Console
    #{setPasswordLink}
    <---------- New User - Set Password ---------->
    """

    htmlMessageTemplate = (firstname, setPasswordLink) -> """
    <h1>New OpenHIM Profile</h1>
    <p>Hi #{firstname},<br/><br/>A profile has been created for you on the OpenHIM instance running on #{config.alerts.himInstance}</p>
    <p>Follow the below link to set your password and log into OpenHIM Console</p>
    <p>#{setPasswordLink}</p>
    """

    ###
    # Adds a user
    ###
    exports.addUser = ->
      # Test if the user is authorised
      if not authorisation.inGroup 'admin', this.authenticated
        utils.logAndSetResponse this, 403, "User #{this.authenticated.email} is not an admin, API access to addUser denied.", 'info'
        return

      userData = this.request.body

      # Generate the new user token here
      # set locked = true
      # set expiry date = true

      token = randtoken.generate 32
      userData.token = token
      userData.tokenType = 'newUser'
      userData.locked = true

      duration = config.newUserExpiry.duration
      durationType = config.newUserExpiry.durationType
      userData.expiry = moment().add(duration, durationType).utc().format()

      consoleURL = config.alerts.consoleURL
      setPasswordLink = "#{consoleURL}/#/set-password/#{token}"

      try
        user = new User userData
        result = yield Q.ninvoke user, 'save'

        # Send email to new user to set password

        plainMessage = plainMessageTemplate userData.firstname, setPasswordLink
        htmlMessage = htmlMessageTemplate userData.firstname, setPasswordLink

        contact.contactUser 'email', userData.email, 'OpenHIM Console Profile', plainMessage, htmlMessage, (err) ->
          if err
            logger.error "The email could not be sent to the user via the API #{err}"
          else
            logger.info 'The email has been sent to the new user'

        this.body = 'User successfully created'
        this.status = 201
        logger.info "User #{this.authenticated.email} created user #{userData.email}"
      catch e
        utils.logAndSetResponse this, 500, "Could not add user via the API #{e}", 'error'

    ###
    # Retrieves the details of a specific user
    ###
    exports.getUser = (email) ->

      email = unescape email

      # Test if the user is authorised, allow a user to fetch their own details
      if not authorisation.inGroup('admin', this.authenticated) and this.authenticated.email isnt email
        utils.logAndSetResponse this, 403, "User #{this.authenticated.email} is not an admin, API access to getUser denied.", 'info'
        return

      try
        result = yield User.findOne(email: email).exec()
        if not result
          this.body = "User with email #{email} could not be found."
          this.status = 404
        else
          this.body = result
      catch e
        utils.logAndSetResponse this, 500, "Could not get user via the API #{e}", 'error'

    exports.updateUser = (email) ->

      email = unescape email

      # Test if the user is authorised, allow a user to update their own details
      if not authorisation.inGroup('admin', this.authenticated) and this.authenticated.email isnt email
        utils.logAndSetResponse this, 403, "User #{this.authenticated.email} is not an admin, API access to updateUser denied.", 'info'
        return

      userData = this.request.body

      # reset token/locked/expiry when user is updated and password supplied
      if userData.passwordAlgorithm and userData.passwordHash and userData.passwordSalt
        userData.token = null
        userData.tokenType = null
        userData.locked = false
        userData.expiry = null

      # Don't allow a non-admin user to change their groups
      if this.authenticated.email is email and not authorisation.inGroup 'admin', this.authenticated then delete userData.groups

      #Ignore _id if it exists (update is by email)
      if userData._id then delete userData._id

      try
        yield User.findOneAndUpdate(email: email, userData).exec()
        this.body = "Successfully updated user."
        logger.info "User #{this.authenticated.email} updated user #{userData.email}"
      catch e
        utils.logAndSetResponse this, 500, "Could not update user #{email} via the API #{e}", 'error'

    exports.removeUser = (email) ->

      # Test if the user is authorised
      if not authorisation.inGroup 'admin', this.authenticated
        utils.logAndSetResponse this, 403, "User #{this.authenticated.email} is not an admin, API access to removeUser denied.", 'info'
        return

      email = unescape email

      # Test if the user is root@openhim.org
      if email is 'root@openhim.org'
        utils.logAndSetResponse this, 403, "User root@openhim.org is OpenHIM root, User cannot be deleted through the API", 'info'
        return

      try
        yield User.findOneAndRemove(email: email).exec()
        this.body = "Successfully removed user with email #{email}"
        logger.info "User #{this.authenticated.email} removed user #{email}"
      catch e
        utils.logAndSetResponse this, 500, "Could not remove user #{email} via the API #{e}", 'error'

    exports.getUsers = ->
      # Test if the user is authorised
      if not authorisation.inGroup 'admin', this.authenticated
        utils.logAndSetResponse this, 403, "User #{this.authenticated.email} is not an admin, API access to getUsers denied.", 'info'
        return

      try
        this.body = yield User.find().exec()
      catch e
        utils.logAndSetResponse this, 500, "Could not fetch all users via the API #{e}", 'error'