Skip to content

Commit

Permalink
Merge branch 'master' into cron
Browse files Browse the repository at this point in the history
  • Loading branch information
jupe committed Jan 13, 2019
2 parents 965ba6f + 65655e6 commit 537e838
Show file tree
Hide file tree
Showing 11 changed files with 392 additions and 2 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ Available [here](doc/APIs)

By default opentmi is started as development mode. You can configure environment using [`--config <file>`](`config.example.json`) -option.

note: `"mongo"` options overwrites defaults and is pypassed to [MongoClient](http://mongodb.github.io/node-mongodb-native/3.0/api/MongoClient.html).
**note**:
* `"mongo"` options overwrites defaults and is pypassed to [MongoClient](http://mongodb.github.io/node-mongodb-native/3.0/api/MongoClient.html).
* `"smtp"` options is pypassed to [nodemailer](https://nodemailer.com/smtp/) transport configurations. To activate smpt use `enabled` property.

# Architecture

Expand Down
58 changes: 58 additions & 0 deletions app/controllers/emailer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 3rd party modules
const _ = require('lodash');
const nodemailer = require('nodemailer');
// internal modules
const logger = require('../tools/logger');


let emailer = {send: () => Promise.reject('emailer is not configured')};


class Emailer {
constructor(config) {
this._smtpTransport = _.get(config, 'enabled') ?
Emailer._initialize(_.omit(config, ['enabled'])) : Emailer.getDummyTransport();
emailer = this;
}
static getDummyTransport() {
logger.info('smtp is not configured, cannot send emails');
return {
verify: () => Promise.resolve(),
sendMail: () => Promise.reject(new Error('smtp is not configured to server'))
};
}
static _initialize(config) {
const mailerConfig = _.defaults(config, {
host: 'smtp.gmail.com',
port: 25,
secure: false,
connectionTimeout: 5000,
tls: {
rejectUnauthorized: false
}
});
return nodemailer.createTransport(mailerConfig);
}
verify() {
return this._smtpTransport.verify()
.then((value) => {
if (value === undefined) {
return;
}
logger.debug('smtp verified - ok');
});
}
static send(data) {
return emailer.send(data);
}
send({to, from = 'admin@opentmi.com', subject, text}) {
const mailOptions = {to, from, subject, text};
logger.silly(`sending mail: ${JSON.stringify(mailOptions)}`);
return this._smtpTransport.sendMail(mailOptions)
.then(() => {
logger.info(`Sent email to "${to}" with subject: "${subject}"`);
});
}
}

module.exports = Emailer;
93 changes: 93 additions & 0 deletions app/controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@

// 3rd party modules
const _ = require('lodash');
const crypto = require('crypto');
const moment = require('moment');
const Promise = require('bluebird');

// Own modules
const config = require('../tools/config');
const Emailer = require('./emailer');
const logger = require('../tools/logger');
const DefaultController = require('./');


Expand Down Expand Up @@ -65,6 +70,94 @@ class UsersController extends DefaultController {
})
.catch(error => res.status(500).json({error: `${error}`}));
}
forgotPassword(req, res) {
return Promise
.try(() => {
const email = req.body.email;
if (!email) {
const error = new Error('missing email address');
error.code = 400;
throw error;
}
return email;
})
.then(email => this.Model.findOne({email}))
.then((theUser) => {
const user = theUser;
if (!user) {
const error = new Error('email not exists');
error.code = 400;
throw error;
}
user.resetPasswordToken = crypto.randomBytes(20).toString('hex');
user.resetPasswordExpires = moment().add(1, 'hours').toDate(); // 1 h
return user.save();
})
.then((user) => {
logger.debug(`User ${user.name} password reset token are: ${user.resetPasswordToken}`);
return UsersController._notifyPasswordToken(user);
})
.then(() => res.status(200).json({message: 'password reset token is sent to you'}))
.catch(UsersController._restCatch.bind(res));
}
static _notifyPasswordToken(user) {
const token = user.resetPasswordToken;
const subject = 'OpenTMI Password Change';
const host = _.get(config.get('github'), 'callbackURL', 'https://opentmi');
const link = `${host}/change-password/${token}`;
const text = UsersController._tokenEmail(link, user.email, token);
return Emailer.send({to: user.email, subject, text});
}
static _tokenEmail(link, email, token) {
return 'You requested a password reset for your OpenTMI account.' +
'In case this request was not initiated by you, you can safely ignore it.\n\n' +
'Your account:\n' +
`Email address: ${email}\n` +
'To reset your password, please click on the link below:\n' +
`${link}\n` +
`Or using token: ${token}`;
}
changePassword(req, res) {
return Promise
.try(() => {
if (!_.has(req, 'body.password') ||
!_.has(req, 'body.token')) {
const error = new Error('Missing token or new password');
error.code = 400;
throw error;
}
})
.then(() => this.Model.findOne({
resetPasswordToken: req.body.token,
resetPasswordExpires: {$gt: Date.now()}
}
))
.then((theUser) => {
const user = theUser;
if (!user) {
const error = new Error('invalid or expired token');
error.code = 401;
throw error;
}
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
return user.save();
})
.then(() => {
res.status(200).json({message: 'password reset was successful'});
})
.catch(UsersController._restCatch.bind(res));
}
static _restCatch(error) {
const body = {message: error.message};
if (process.env.NODE_ENV !== 'production') {
body.stack = error.stack;
logger.error(error);
logger.error(error.stack);
}
this.status(error.code || 500).json(body);
}
}

module.exports = UsersController;
4 changes: 4 additions & 0 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const express = require('./express');
const Server = require('./server');
const models = require('./models');
const routes = require('./routes');
const Emailer = require('./controllers/emailer');
const AddonManager = require('./addons');
const logger = require('./tools/logger');
const config = require('./tools/config');
Expand Down Expand Up @@ -43,12 +44,15 @@ const io = SocketIO(server);
const ioAdapter = mongoAdapter(dbUrl);
io.adapter(ioAdapter);

const emailer = new Emailer(config.get('smtp'));

// Initialize database connection
DB.connect()
.catch((error) => {
console.error('mongoDB connection failed: ', error.stack); // eslint-disable-line no-console
process.exit(-1);
})
.then(() => emailer.verify())
.then(() => models.registerModels())
.then(() => express(app))
.then(() => routes.registerRoutes(app, io))
Expand Down
5 changes: 5 additions & 0 deletions app/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const UserSchema = new Schema({
name: {type: String, required: true},
email: {type: String, unique: true, sparse: true, lowercase: true},
password: {type: String, select: false},

// for recovering
resetPasswordToken: {type: String},
resetPasswordExpires: {type: Date},

displayName: String,
picture: String,
bitbucket: String,
Expand Down
5 changes: 5 additions & 0 deletions app/routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ function Route(app) {
userRouter.use('/:User', singleUserRouter);
app.use('/api/v0/users', userRouter);

// password recovery
app.post('/api/v0/password/forgot', userController.forgotPassword.bind(userController));
app.post('/api/v0/password/change', userController.changePassword.bind(userController));


const authRoute = express.Router();
authRoute
.post('/login', passport.authenticate('local'),
Expand Down
10 changes: 10 additions & 0 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@
"user": "admin",
"pwd": "admin"
},
"smtp": {
"enabled": false,
"host": "smtp.gmail.com",
"port": 465,
"secure": true,
"auth": {
"user": "<username>",
"pass": "<password>"
}
},
"mongo": {
"sslValidate": true
},
Expand Down
63 changes: 62 additions & 1 deletion doc/APIs/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,65 @@ GET /api/v0/users/:User/apikeys/new
## Fetch Delete apikey
```
DELETE /api/v0/users/:User/apikeys/:Key
```
```


## recover forgot password

To request recovery token to user email

* ##### URL
/api/v0/password/forgot

* ##### Method:
`POST`

* ##### Request:
**Content:**
```json
{ "email": "my-email@address.com" }
```

* ##### Success Response:
* **Code:** 200
**Content:**
```json
{ "message": "success" }
```

* ##### Error Response:
* **Code:** 400 UNAUTHORIZED
**Content:**
```json
{ "message": "No authorization token was found" }
```

## change password using recovery token

To change password using token that was send when previous API was used.

* ##### URL
/api/v0/password/change

* ##### Method:
`POST`

* ##### Request:
**Content:**
```json
{ "token": "<token>", "password": "<new-password>" }
```

* ##### Success Response:
* **Code:** 200
**Content:**
```json
{ "message": "success" }
```

* ##### Error Response:
* **Code:** 400 UNAUTHORIZED
**Content:**
```json
{ "message": "<message>" }
```
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"mongoose-query": "^0.5.3",
"mongoose-schema-jsonschema": "1.2.1",
"nconf": "^0.10.0",
"nodemailer": "^5.1.1",
"opentmi-addon": "github:opentmi/opentmi-addon",
"passport": "^0.4.0",
"passport-github-token": "^2.1.0",
Expand Down
Loading

0 comments on commit 537e838

Please sign in to comment.