diff --git a/nodejs/middleware/auth.js b/nodejs/middleware/auth.js index 6b98731..808d57b 100755 --- a/nodejs/middleware/auth.js +++ b/nodejs/middleware/auth.js @@ -5,7 +5,8 @@ const {Auth} = require('../models/auth'); async function auth(req, res, next){ try{ let user = await Auth.checkToken({token: req.header('auth-token')}); - if(user.username){ + + if(user.uid){ req.user = user; return next(); } diff --git a/nodejs/models/auth.js b/nodejs/models/auth.js index e5d68af..42e2dba 100644 --- a/nodejs/models/auth.js +++ b/nodejs/models/auth.js @@ -1,12 +1,14 @@ +'use strict'; + const {User} = require('./user'); const {Token, AuthToken} = require('./token'); -Auth = {} +var Auth = {} Auth.errors = {} Auth.errors.login = function(){ - let error = new Error('PamLoginFailed'); - error.name = 'PamLoginFailed'; + let error = new Error('LDAPLoginFailed'); + error.name = 'LDAPLoginFailed'; error.message = `Invalid Credentials, login failed.`; error.status = 401; diff --git a/nodejs/models/email.js b/nodejs/models/email.js new file mode 100644 index 0000000..4dc8bc2 --- /dev/null +++ b/nodejs/models/email.js @@ -0,0 +1,32 @@ +'use strict'; + +const sgMail = require('@sendgrid/mail'); +const mustache = require('mustache'); +const conf = require('../app').conf; + +sgMail.setApiKey(conf.SENDGRID_API_KEY); + +var Mail = {}; + +Mail.send = async function(to, subject, message, from){ + await sgMail.send({ + to: to, + from: from || 'Theta 42 Accounts ', + subject: subject, + text: message, + html: message, + }); +}; + + +Mail.sendTemplate = async function(to, template, context, from){ + template = require(`../views/email_templates/${template}`); + await Mail.send( + to, + mustache.render(template.subject, context), + mustache.render(template.message, context), + from || (template.from && mustache.render(template.message, context)) + ) +}; + +module.exports = {Mail}; diff --git a/nodejs/models/group_ldap.js b/nodejs/models/group_ldap.js index a48c482..2c33609 100644 --- a/nodejs/models/group_ldap.js +++ b/nodejs/models/group_ldap.js @@ -4,7 +4,7 @@ const { Client, Attribute, Change } = require('ldapts'); const conf = require('../app').conf.ldap; const client = new Client({ - url: conf.url, + url: conf.url, }); async function getGroups(client){ @@ -25,21 +25,21 @@ async function getGroups(client){ } async function addGroup(client, data){ - try{ + try{ - await client.add(`cn=${data.name},${conf.groupBase}`, { - cn: data.name, - member: data.owner, - description: data.description, - owner: data.owner, - objectclass: [ 'groupOfNames', 'top' ] - }); + await client.add(`cn=${data.name},${conf.groupBase}`, { + cn: data.name, + member: data.owner, + description: data.description, + owner: data.owner, + objectclass: [ 'groupOfNames', 'top' ] + }); - return data; + return data; - }catch(error){ - throw error; - } + }catch(error){ + throw error; + } } async function addMember(client, group, user){ @@ -61,18 +61,18 @@ async function addMember(client, group, user){ async function removeMember(client, group, user){ try{ - await client.modify(group.dn, [ - new Change({ - operation: 'delete', - modification: new Attribute({ - type: 'member', - values: [user.dn] - })}), - ]); - }catch(error){ - if(error = "TypeOrValueExistsError")return ; - throw error; - } + await client.modify(group.dn, [ + new Change({ + operation: 'delete', + modification: new Attribute({ + type: 'member', + values: [user.dn] + })}), + ]); + }catch(error){ + if(error = "TypeOrValueExistsError")return ; + throw error; + } } @@ -134,4 +134,19 @@ Group.get = async function(data){ } } +Group.add = async function(data){ + try{ + await client.bind(conf.bindDN, conf.bindPassword); + + await addGroup(client, data); + + await client.unbind(); + + return this.get(data); + + }catch(error){ + + } +} + module.exports = {Group}; diff --git a/nodejs/models/token.js b/nodejs/models/token.js index 496e26c..9b0ac11 100644 --- a/nodejs/models/token.js +++ b/nodejs/models/token.js @@ -29,7 +29,9 @@ Token.check = async function(data){ var InviteToken = Object.create(Token({ name: 'invite', keyMap:{ - claimed_by: {default:"__NONE__", isRequired: false, type: 'string',} + claimed_by: {default:"__NONE__", isRequired: false, type: 'string',}, + mail: {default:"__NONE__", isRequired: false, type: 'string',}, + mail_token: {default: UUID, type: 'string', min: 36, max: 36}, } })); @@ -53,7 +55,7 @@ var AuthToken = Object.create(Token({ })); AuthToken.add = async function(data){ - data.created_by = data.username; + data.created_by = data.uid; return AuthToken.__proto__.add(data); }; diff --git a/nodejs/models/user_ldap.js b/nodejs/models/user_ldap.js index 17842de..59eb631 100644 --- a/nodejs/models/user_ldap.js +++ b/nodejs/models/user_ldap.js @@ -3,6 +3,7 @@ const { Client, Attribute, Change } = require('ldapts'); const crypto = require('crypto'); +const {Mail} = require('./email'); const {Token, InviteToken} = require('./token'); const conf = require('../app').conf.ldap; @@ -47,7 +48,9 @@ async function addPosixAccount(client, data){ uid: data.uid, uidNumber: data.uidNumber, gidNumber: data.gidNumber, + givenName: data.givenName, mail: data.mail, + mobile: data.mobile, loginShell: data.loginShell, homeDirectory: data.homeDirectory, userPassword: data.userPassword, @@ -81,6 +84,14 @@ async function addLdapUser(client, data){ } } +async function deleteLdapUser(client, data){ + try{ + await client.del(`cn=${data.cn},${conf.groupBase}`); + await client.del(data.dn); + }catch(error){ + throw error; + } +} async function changeLdapPassword(client, data){ try{ @@ -100,14 +111,8 @@ async function changeLdapPassword(client, data){ const user_parse = function(data){ if(data[conf.userNameAttribute]){ data.username = data[conf.userNameAttribute] - // delete data[conf.userNameAttribute]; } - // if(data.uidNumber){ - // data.uid = data.uidNumber; - // delete data.uidNumber; - // } - return data; } @@ -115,11 +120,6 @@ var User = {} User.backing = "LDAP"; -User.keyMap = { - 'username': {isRequired: true, type: 'string', min: 3, max: 500}, - 'password': {isRequired: true, type: 'string', min: 3, max: 500}, -} - User.list = async function(){ try{ await client.bind(conf.bindDN, conf.bindPassword); @@ -170,14 +170,14 @@ User.listDetail = async function(){ User.get = async function(data){ try{ if(typeof data !== 'object'){ - let username = data; + let uid = data; data = {}; - data.username = username; + data.uid = uid; } await client.bind(conf.bindDN, conf.bindPassword); - let filter = `(&${conf.userFilter}(${conf.userNameAttribute}=${data.username}))`; + let filter = `(&${conf.userFilter}(${conf.userNameAttribute}=${data.uid}))`; const res = await client.search(conf.userBase, { scope: 'sub', @@ -225,7 +225,18 @@ User.add = async function(data) { await client.unbind(); - return this.get(data.uid); + let user = await this.get(data.uid); + + + await Mail.sendTemplate( + user.mail, + 'welcome', + { + user: user + } + ) + + return user; }catch(error){ if(error.message.includes('exists')){ @@ -244,7 +255,7 @@ User.addByInvite = async function(data){ try{ let token = await InviteToken.get(data.token); - if(!token.is_valid){ + if(!token.is_valid && data.mailToken !== token.mail_token){ let error = new Error('Token Invalid'); error.name = 'Token Invalid'; error.message = `Token is not valid or as allready been used. ${data.token}`; @@ -252,6 +263,8 @@ User.addByInvite = async function(data){ throw error; } + data.mail = token.mail; + let user = await this.add(data); if(user){ @@ -265,13 +278,37 @@ User.addByInvite = async function(data){ }; -// User.remove = async function(data){ -// try{ -// return await linuxUser.removeUser(this.username); -// }catch(error){ -// throw error; -// } -// }; +User.verifyEmail = async function(data){ + try{ + let token = await InviteToken.get(data.token); + await token.update({mail: data.mail}) + await Mail.sendTemplate( + data.mail, + 'validate_link', + { + link:`${data.url}/login/invite/${token.token}/${token.mail_token}` + } + ) + }catch(error){ + throw error; + } +}; + +User.remove = async function(data){ + try{ + + await client.bind(conf.bindDN, conf.bindPassword); + + await deleteLdapUser(client, this); + + await client.unbind(); + + return true + + }catch(error){ + throw error; + } +}; // User.setPassword = async function(data){ // try{ diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index f58349e..cdb4a3d 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -444,11 +444,21 @@ "brace-expansion": "^1.1.7" } }, + "moment": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz", + "integrity": "sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "mustache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.0.1.tgz", + "integrity": "sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA==" + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", diff --git a/nodejs/package.json b/nodejs/package.json index d0ddd0d..80bd6e5 100755 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -17,6 +17,8 @@ "express": "~4.16.1", "extend": "^3.0.2", "ldapts": "^2.2.1", + "moment": "^2.25.3", + "mustache": "^4.0.1", "redis": "^2.8.0", "smtpc": "^0.1.2" }, diff --git a/nodejs/public/css/styles.css b/nodejs/public/css/styles.css index e69de29..ad05db6 100755 --- a/nodejs/public/css/styles.css +++ b/nodejs/public/css/styles.css @@ -0,0 +1,13 @@ + +.hover-effect { + transition: all .5s ease; +} + +.hover-effect:hover { + color: darkblue; + transition: all .5s ease; + background-color: inherit; + -ms-transform: scale(1.1); + -webkit-transform: scale(1.1); + transform: scale(1.1); /* Standard syntax */ +} diff --git a/nodejs/public/js/app.js b/nodejs/public/js/app.js index a1837be..f7415ea 100755 --- a/nodejs/public/js/app.js +++ b/nodejs/public/js/app.js @@ -112,8 +112,9 @@ app.auth = (function(app) { }); } - function logOut(){ + function logOut(callack){ localStorage.removeItem('APIToken'); + callack(); } function makeUserFromInvite(args, callack){ @@ -150,13 +151,13 @@ app.user = (function(app){ } function remove(args, callack){ - app.api.delete('user/'+ args.username, function(error, data){ + app.api.delete('user/'+ args.uid, function(error, data){ callack(error, data); }); } function changePassword(args, callack){ - app.api.put('users/'+ arg.username || '', args, function(error, data){ + app.api.put('users/'+ arg.uid || '', args, function(error, data){ callack(error, data); }); } @@ -313,8 +314,6 @@ function formAJAX( btn, del ) { var formData = $form.find( '[name]' ).serializeObject(); // builds query formDataing var method = $form.attr('method') || 'post'; - console.log('formAJAX method', method) - if( !$form.validate( { form: { @@ -326,9 +325,9 @@ function formAJAX( btn, del ) { } app.api[method]($form.attr( 'action' ), formData, function(error, data){ - tableAJAX( data.message ); //re-populate table + app.util.actionMessage( data.message ); //re-populate table if(!error){ - eval( $form.attr( 'evalAJAX' ) ); //gets JS to run after completion + eval($form.attr('evalAJAX')); //gets JS to run after completion } }); diff --git a/nodejs/public/js/val.js b/nodejs/public/js/val.js index d5a75bc..331295a 100755 --- a/nodejs/public/js/val.js +++ b/nodejs/public/js/val.js @@ -20,8 +20,8 @@ return; } - $( '' ).html( ' - ' + error_message ).appendTo( $input.siblings( 'label' ) ); - $input.parent().addClass("has-error"); + $( '' ).html( ' - ' + error_message ).appendTo( $input.parents('.form-group').children( 'label' ) ); + $input.addClass("is-invalid"); failedCount++; return false; } @@ -35,8 +35,8 @@ value = $input.val(), //link to input value rule = attr[0]; - $input.siblings( 'label' ).children( 'b' ).remove(); //removes old error - $input.parent().removeClass( "has-error" ); //removes has-error class + $input.parents('.form-group').children( 'label' ).children( 'b' ).remove(); //removes old error + $input.removeClass( "is-invalid" ); //removes is-invalid class //checks if field is required, and length if (isNaN(requirement) === false && requirement && value.length < requirement) { diff --git a/nodejs/routes/auth.js b/nodejs/routes/auth.js index 3c5aa0d..311e016 100755 --- a/nodejs/routes/auth.js +++ b/nodejs/routes/auth.js @@ -29,14 +29,15 @@ router.all('/logout', async function(req, res, next){ } }); -router.post('/invite/:token', async function(req, res, next) { +router.post('/invite/:token/:mailToken', async function(req, res, next) { try{ req.body.token = req.params.token; + req.body.mailToken = req.params.mailToken; let user = await User.addByInvite(req.body); let token = await AuthToken.add(user); return res.json({ - user: user.username, + user: user.uid, token: token.token }); @@ -46,6 +47,21 @@ router.post('/invite/:token', async function(req, res, next) { }); +router.post('/invite/:token', async function(req, res, next){ + try{ + let data = { + token: req.params.token, + url: `${req.protocol}://${req.hostname}`, + mail: req.body.mail, + } + + await User.verifyEmail(data); + return res.send({message: 'sent'}); + }catch(error){ + next(error) + } +}); + module.exports = router; /* diff --git a/nodejs/routes/index.js b/nodejs/routes/index.js index 9073ba7..287a6ee 100755 --- a/nodejs/routes/index.js +++ b/nodejs/routes/index.js @@ -1,7 +1,11 @@ +'use strict'; + var express = require('express'); var router = express.Router(); +const moment = require('moment'); const {InviteToken} = require('./../models/token'); + /* GET home page. */ router.get('/', async function(req, res, next) { res.render('home', { title: 'Express' }); @@ -12,21 +16,38 @@ router.get('/users', function(req, res, next) { res.render('users', { title: 'Express' }); }); -router.get('/login/invite/:token', async function(req, res, next){ +router.get('/login/invite/:token/:mailToken', async function(req, res, next){ try{ - console.log('token', req.params.token) + let token = await InviteToken.get(req.params.token); - console.log('invite', token); - if(token.is_valid){ + + if(token.is_valid && token.mail !== '__NONE__' && token.mail_token === req.params.mailToken){ + token.created_on = moment(token.created_on, 'x').fromNow(); res.render('invite', { title: 'Express', invite: token }); }else{ - next({status: 404}); + next({message: 'token not found', status: 404}); } }catch(error){ next(error); } }); +router.get('/login/invite/:token', async function(req, res, next){ + try{ + let token = await InviteToken.get(req.params.token); + token.created_on = moment(token.created_on, 'x').fromNow(); + + if(token.is_valid){ + res.render('invite_email', { title: 'Express', invite: token }); + }else{ + next({message: 'token not found', status: 404}); + } + }catch(error){ + next(error); + } +}); + + /* GET home page. */ router.get('/login', function(req, res, next) { res.render('login', {redirect: req.query.redirect}); diff --git a/nodejs/routes/user.js b/nodejs/routes/user.js index 06c054e..cb41914 100755 --- a/nodejs/routes/user.js +++ b/nodejs/routes/user.js @@ -15,7 +15,7 @@ router.get('/', async function(req, res, next){ router.post('/', async function(req, res, next){ try{ - req.body.created_by = req.user.username + req.body.created_by = req.user.uid return res.json({results: await User.add(req.body)}); }catch(error){ @@ -23,11 +23,13 @@ router.post('/', async function(req, res, next){ } }); -router.delete('/:username', async function(req, res, next){ +router.delete('/:uid', async function(req, res, next){ try{ - let user = await User.get(req.params.username); + let user = await User.get(req.params.uid); - return res.json({username: req.params.username, results: await user.remove()}) + console.log('delete user', user); + + return res.json({uid: req.params.uid, results: await user.remove()}) }catch(error){ next(error); } @@ -36,7 +38,7 @@ router.delete('/:username', async function(req, res, next){ router.get('/me', async function(req, res, next){ try{ - return res.json(await User.get({username: req.user.username})); + return res.json(await User.get({uid: req.user.uid})); }catch(error){ next(error); } @@ -50,9 +52,9 @@ router.put('/password', async function(req, res, next){ } }); -router.put('/password/:username', async function(req, res, next){ +router.put('/password/:uid', async function(req, res, next){ try{ - let user = await User.get(req.params.username); + let user = await User.get(req.params.uid); return res.json({results: await user.setPassword(req.body)}); }catch(error){ next(error); @@ -72,7 +74,7 @@ router.post('/invite', async function(req, res, next){ router.post('/key', async function(req, res, next){ try{ let added = await User.addSSHkey({ - username: req.user.username, + uid: req.user.uid, key: req.body.key }); diff --git a/nodejs/testmail.js b/nodejs/testmail.js deleted file mode 100644 index 2ab103c..0000000 --- a/nodejs/testmail.js +++ /dev/null @@ -1,13 +0,0 @@ - -// using Twilio SendGrid's v3 Node.js Library -// https://github.com/sendgrid/sendgrid-nodejs -const sgMail = require('@sendgrid/mail'); -sgMail.setApiKey(process.env.SENDGRID_API_KEY); -const msg = { - to: 'wmantly@gmail.com', - from: 'info@no-reply.theta42.com', - subject: 'Sending with Twilio SendGrid is Fun', - text: 'and easy to do anywhere, even with Node.js', - html: 'and easy to do anywhere, even with Node.js', -}; -sgMail.send(msg); \ No newline at end of file diff --git a/nodejs/utils/redis.js b/nodejs/utils/redis.js index e12e6e3..e032540 100755 --- a/nodejs/utils/redis.js +++ b/nodejs/utils/redis.js @@ -1,8 +1,10 @@ +'use strict'; + const {createClient} = require('redis'); const {promisify} = require('util'); const config = { - prefix: 'proxy_' + prefix: 'sso_' } function client() { diff --git a/nodejs/views/email_templates/untitled b/nodejs/views/email_templates/untitled new file mode 100644 index 0000000..e69de29 diff --git a/nodejs/views/email_templates/validate_link.js b/nodejs/views/email_templates/validate_link.js new file mode 100644 index 0000000..ef608c6 --- /dev/null +++ b/nodejs/views/email_templates/validate_link.js @@ -0,0 +1,24 @@ +module.exports = { + subject: 'Validate email for Theta 42 account', + message: ` +

Theta 42 account

+ +

+ Welcome, +

+ +

+ We need to verify the provided email address in order to continue. Please + follow the link below to verify this email address: +

+ +

+ {{ link }} +

+ +

+ Thank you,
+ Theta 42 +

+` +}; diff --git a/nodejs/views/email_templates/welcome.js b/nodejs/views/email_templates/welcome.js new file mode 100644 index 0000000..41b6c80 --- /dev/null +++ b/nodejs/views/email_templates/welcome.js @@ -0,0 +1,34 @@ +module.exports = { + subject: 'Welcome to Theta 42!', + message: ` +

+ Welcome {{user.givenName}}, +

+ +

+ Your new Theta 42 Single sign-on account is ready to use. Here is some + information to get you started. +

+ +

+ Your username is {{user.uid}} +

+ +

+ You can manage your account at https://sso.theta42.com +

+ +

+ You account is ready to be used now, test it by SSHing into the Theta 42 + jump host \`ssh {{user.uid}}@718it.biz\` +

+ +

+ The SSO service is still in beta, so please report any bugs you may find! + You will be notified of new features and services as they become available. +

+ Thank you,
+ Theta 42 +

+` +}; diff --git a/nodejs/views/home.ejs b/nodejs/views/home.ejs index c839ee9..058dde7 100644 --- a/nodejs/views/home.ejs +++ b/nodejs/views/home.ejs @@ -11,8 +11,6 @@ LDAP DN: {{dn}}
Joined {{createTimestamp}}
Joined {{modifyTimestamp}}
- -