<commit_before>(function () {

  // internal verifier collection. Never published.
  Meteor.accounts._srpChallenges = new Meteor.Collection(
    "accounts._srpChallenges",
    null /*manager*/,
    null /*driver*/,
    true /*preventAutopublish*/);

  var selectorFromUserQuery = function (user) {
    if (!user)
      throw new Meteor.Error(400, "Must pass a user property in request");
    if (_.keys(user).length !== 1)
      throw new Meteor.Error(400, "User property must have exactly one field");

    var selector;
    if (user.id)
      selector = {_id: user.id};
    else if (user.username)
      selector = {username: user.username};
    else if (user.email)
      selector = {"emails.email": user.email};
    else
      throw new Meteor.Error(400, "Must pass username, email, or id in request.user");

    return selector;
  };

  Meteor.methods({
    // @param request {Object} with fields:
    //   user: either {username: (username)}, {email: (email)}, or {id: (userId)}
    //   A: hex encoded int. the client's public key for this exchange
    // @returns {Object} with fields:
    //   identiy: string uuid
    //   salt: string uuid
    //   B: hex encoded int. server's public key for this exchange
    beginPasswordExchange: function (request) {
      var selector = selectorFromUserQuery(request.user);

      var user = Meteor.users.findOne(selector);
      if (!user)
        throw new Meteor.Error(403, "User not found");

      if (!user.services || !user.services.password ||
          !user.services.password.srp)
        throw new Meteor.Error(403, "User has no password set");

      var verifier = user.services.password.srp;
      var srp = new Meteor._srp.Server(verifier);
      var challenge = srp.issueChallenge({A: request.A});

      // XXX It would be better to put this on the session
      // somehow. However, this gets complicated when interacting with
      // reconnect on the client. The client should detect the reconnect
      // and re-start the exchange.
      // https://app.asana.com/0/988582960612/1278583012594
      //
      // Instead we store M and HAMK from SRP (abstraction violation!)
      // and let any session login if it knows M. This is somewhat
      // insecure, if you don't use SSL someone can sniff your traffic
      // and then log in as you (but no more insecure than reconnect
      // tokens).
      var serialized = { userId: user._id, M: srp.M, HAMK: srp.HAMK };
      Meteor.accounts._srpChallenges.insert(serialized);

      return challenge;
    },

    changePassword: function (options) {
      if (!this.userId())
        throw new Meteor.Error(401, "Must be logged in");

      // If options.M is set, it means we went through a challenge with
      // the old password.

      if (!options.M && !Meteor.accounts._options.unsafePasswordChanges) {
        throw new Meteor.Error(403, "Old password required.");
      }

      if (options.M) {
        var serialized = Meteor.accounts._srpChallenges.findOne(
          {M: options.M});
        if (!serialized)
          throw new Meteor.Error(403, "Incorrect password");
        if (serialized.userId !== this.userId())
          // No monkey business!
          throw new Meteor.Error(403, "Incorrect password");
      }

      var verifier = options.srp;
      if (!verifier && options.password) {
        verifier = Meteor._srp.generateVerifier(options.password);
      }
      if (!verifier || !verifier.identity || !verifier.salt ||
          !verifier.verifier)
        throw new Meteor.Error(400, "Invalid verifier");

      Meteor.users.update({_id: this.userId()},
                          {$set: {'services.password.srp': verifier}});

      var ret = {passwordChanged: true};
      if (serialized)
        ret.HAMK = serialized.HAMK;
      return ret;
    },

    forgotPassword: function (options) {
      var email = options.email;
      var baseUrl = options.baseUrl;
      if (!email)
        throw new Meteor.Error(400, "Need to set options.email");
      if (!baseUrl)
        throw new Meteor.Error(400, "Need to set options.baseUrl");

      var user = Meteor.users.findOne({"emails.email": email});
      if (!user)
        throw new Meteor.Error(403, "User not found");

      var token = Meteor.uuid();
      var creationTime = +(new Date);
      Meteor.users.update(user._id, {$set: {
        "services.password.reset": {
          token: token,
          creationTime: creationTime
        }
      }});

      // XXX definitely *not* the final form!
      Meteor.mail.send(email, Meteor.accounts.urls.resetPassword(baseUrl, token));
    },

    resetPassword: function (token, newVerifier) {
      if (!token)
        throw new Meteor.Error(400, "Need to pass token");
      if (!newVerifier)
        throw new Meteor.Error(400, "Need to pass newVerifier");

      var user = Meteor.users.findOne({"services.password.reset.token": token});
      if (!user)
        throw new Meteor.Error(403, "Reset password link expired");

      Meteor.users.update({_id: user._id}, {
        $set: {'services.password.srp': newVerifier},
        $unset: {'services.password.reset': 1}
      });

      var loginToken = Meteor.accounts._loginTokens.insert({userId: user._id});
      this.setUserId(user._id);
      return {token: loginToken, id: user._id};
    },

    enrollAccount: function (token, newVerifier) {
      if (!token)
        throw new Meteor.Error(400, "Need to pass token");
      if (!newVerifier)
        throw new Meteor.Error(400, "Need to pass newVerifier");

      var user = Meteor.users.findOne({"services.password.enroll.token": token});
      if (!user)
        throw new Meteor.Error(403, "Enroll account link expired");

      Meteor.users.update({_id: user._id}, {
        $set: {'services.password.srp': newVerifier},
        $unset: {'services.password.enroll': 1}
      });
      Meteor.users.update({_id: user._id},
                          {$set: {"emails.0.validated": true}});

      var loginToken = Meteor.accounts._loginTokens.insert({userId: user._id});
      this.setUserId(user._id);
      return {token: loginToken, id: user._id};
    },

    validateEmail: function (token) {
      if (!token)
        throw new Meteor.Error(400, "Need to pass token");

      var tokenDocument = Meteor.accounts._emailValidationTokens.findOne(
        {token: token});
      if (!tokenDocument)
        throw new Meteor.Error(403, "Validate email link expired");
      var userId = tokenDocument.userId;
      var email = tokenDocument.email;
      Meteor.users.update({_id: userId, "emails.email": email},
                          {$set: {"emails.$.validated": true}});
      Meteor.accounts._emailValidationTokens.remove({token: token});

      var loginToken = Meteor.accounts._loginTokens.insert({userId: userId});
      this.setUserId(userId);
      return {token: loginToken, id: userId};
    }<commit_msg>Added comment explaining the use of the mongo positional operator<commit_after>(function () {

  // internal verifier collection. Never published.
  Meteor.accounts._srpChallenges = new Meteor.Collection(
    "accounts._srpChallenges",
    null /*manager*/,
    null /*driver*/,
    true /*preventAutopublish*/);

  var selectorFromUserQuery = function (user) {
    if (!user)
      throw new Meteor.Error(400, "Must pass a user property in request");
    if (_.keys(user).length !== 1)
      throw new Meteor.Error(400, "User property must have exactly one field");

    var selector;
    if (user.id)
      selector = {_id: user.id};
    else if (user.username)
      selector = {username: user.username};
    else if (user.email)
      selector = {"emails.email": user.email};
    else
      throw new Meteor.Error(400, "Must pass username, email, or id in request.user");

    return selector;
  };

  Meteor.methods({
    // @param request {Object} with fields:
    //   user: either {username: (username)}, {email: (email)}, or {id: (userId)}
    //   A: hex encoded int. the client's public key for this exchange
    // @returns {Object} with fields:
    //   identiy: string uuid
    //   salt: string uuid
    //   B: hex encoded int. server's public key for this exchange
    beginPasswordExchange: function (request) {
      var selector = selectorFromUserQuery(request.user);

      var user = Meteor.users.findOne(selector);
      if (!user)
        throw new Meteor.Error(403, "User not found");

      if (!user.services || !user.services.password ||
          !user.services.password.srp)
        throw new Meteor.Error(403, "User has no password set");

      var verifier = user.services.password.srp;
      var srp = new Meteor._srp.Server(verifier);
      var challenge = srp.issueChallenge({A: request.A});

      // XXX It would be better to put this on the session
      // somehow. However, this gets complicated when interacting with
      // reconnect on the client. The client should detect the reconnect
      // and re-start the exchange.
      // https://app.asana.com/0/988582960612/1278583012594
      //
      // Instead we store M and HAMK from SRP (abstraction violation!)
      // and let any session login if it knows M. This is somewhat
      // insecure, if you don't use SSL someone can sniff your traffic
      // and then log in as you (but no more insecure than reconnect
      // tokens).
      var serialized = { userId: user._id, M: srp.M, HAMK: srp.HAMK };
      Meteor.accounts._srpChallenges.insert(serialized);

      return challenge;
    },

    changePassword: function (options) {
      if (!this.userId())
        throw new Meteor.Error(401, "Must be logged in");

      // If options.M is set, it means we went through a challenge with
      // the old password.

      if (!options.M && !Meteor.accounts._options.unsafePasswordChanges) {
        throw new Meteor.Error(403, "Old password required.");
      }

      if (options.M) {
        var serialized = Meteor.accounts._srpChallenges.findOne(
          {M: options.M});
        if (!serialized)
          throw new Meteor.Error(403, "Incorrect password");
        if (serialized.userId !== this.userId())
          // No monkey business!
          throw new Meteor.Error(403, "Incorrect password");
      }

      var verifier = options.srp;
      if (!verifier && options.password) {
        verifier = Meteor._srp.generateVerifier(options.password);
      }
      if (!verifier || !verifier.identity || !verifier.salt ||
          !verifier.verifier)
        throw new Meteor.Error(400, "Invalid verifier");

      Meteor.users.update({_id: this.userId()},
                          {$set: {'services.password.srp': verifier}});

      var ret = {passwordChanged: true};
      if (serialized)
        ret.HAMK = serialized.HAMK;
      return ret;
    },

    forgotPassword: function (options) {
      var email = options.email;
      var baseUrl = options.baseUrl;
      if (!email)
        throw new Meteor.Error(400, "Need to set options.email");
      if (!baseUrl)
        throw new Meteor.Error(400, "Need to set options.baseUrl");

      var user = Meteor.users.findOne({"emails.email": email});
      if (!user)
        throw new Meteor.Error(403, "User not found");

      var token = Meteor.uuid();
      var creationTime = +(new Date);
      Meteor.users.update(user._id, {$set: {
        "services.password.reset": {
          token: token,
          creationTime: creationTime
        }
      }});

      // XXX definitely *not* the final form!
      Meteor.mail.send(email, Meteor.accounts.urls.resetPassword(baseUrl, token));
    },

    resetPassword: function (token, newVerifier) {
      if (!token)
        throw new Meteor.Error(400, "Need to pass token");
      if (!newVerifier)
        throw new Meteor.Error(400, "Need to pass newVerifier");

      var user = Meteor.users.findOne({"services.password.reset.token": token});
      if (!user)
        throw new Meteor.Error(403, "Reset password link expired");

      Meteor.users.update({_id: user._id}, {
        $set: {'services.password.srp': newVerifier},
        $unset: {'services.password.reset': 1}
      });

      var loginToken = Meteor.accounts._loginTokens.insert({userId: user._id});
      this.setUserId(user._id);
      return {token: loginToken, id: user._id};
    },

    enrollAccount: function (token, newVerifier) {
      if (!token)
        throw new Meteor.Error(400, "Need to pass token");
      if (!newVerifier)
        throw new Meteor.Error(400, "Need to pass newVerifier");

      var user = Meteor.users.findOne({"services.password.enroll.token": token});
      if (!user)
        throw new Meteor.Error(403, "Enroll account link expired");

      Meteor.users.update({_id: user._id}, {
        $set: {'services.password.srp': newVerifier},
        $unset: {'services.password.enroll': 1}
      });
      Meteor.users.update({_id: user._id},
                          {$set: {"emails.0.validated": true}});

      var loginToken = Meteor.accounts._loginTokens.insert({userId: user._id});
      this.setUserId(user._id);
      return {token: loginToken, id: user._id};
    },

    validateEmail: function (token) {
      if (!token)
        throw new Meteor.Error(400, "Need to pass token");

      var tokenDocument = Meteor.accounts._emailValidationTokens.findOne(
        {token: token});
      if (!tokenDocument)
        throw new Meteor.Error(403, "Validate email link expired");
      var userId = tokenDocument.userId;
      var email = tokenDocument.email;

      // update the validated flag on the index in the emails array
      // matching email (see
      // http://www.mongodb.org/display/DOCS/Updating/#Updating-The%24positionaloperator)
      Meteor.users.update({_id: userId, "emails.email": email},
                          {$set: {"emails.$.validated": true}});
      Meteor.accounts._emailValidationTokens.remove({token: token});

      var loginToken = Meteor.accounts._loginTokens.insert({userId: userId});
      this.setUserId(userId);
      return {token: loginToken, id: userId};
    }
{'fraction_non_alphanumeric': 0.12871004566210045, 'fraction_numerical': 0.01341324200913242, 'mean_word_length': 2.527176648213387, 'pattern_counts': {'":': 16, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 2, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '406916', 'n_tokens_mistral': 4395, 'n_tokens_neox': 4130, 'n_words': 1170}