From c81c87c497fdd51b547121b09fb93590e23c82f9 Mon Sep 17 00:00:00 2001 From: John Grigutis Date: Thu, 29 Jul 2021 18:17:12 +0000 Subject: [PATCH 1/6] Adds Node.js specific gitignore --- .gitignore | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 125 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b92cd40..47abd0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,137 @@ +## Node.js (2021-05-25) +## https://github.com/github/gitignore/blob/master/Node.gitignore + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + + +## From mj82 (2019-06-26) + **/barn ui/bower_components -node_modules api/config/auth.* api/config/*.password api/config/index.js api/config/*.jwt ui/config.js nohup*.out -**/*.log debug -coverage docker/config docker/db .*.swp From e4e393aca8f4bf96e4d84c86a9e6fd1ee7c4438c Mon Sep 17 00:00:00 2001 From: John Grigutis Date: Fri, 30 Jul 2021 14:51:07 +0000 Subject: [PATCH 2/6] Ignores package-lock.json Since `npm ci` isn't being used, it's not necessary to track package-lock.json. --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 47abd0a..770a0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,8 @@ docker/config docker/db .*.swp .*.swo + + +## From grigutis (2021-07-30) + +**/package-lock.json From 44be95268f6ce67d2afcc19d9dd4eb7fc80bc04d Mon Sep 17 00:00:00 2001 From: John Grigutis Date: Thu, 26 Aug 2021 20:03:44 +0000 Subject: [PATCH 3/6] Fixes minor errors in devDependencies --- package.json | 99 ++++++++++++++++++++++++++-------------------------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 30ddb4d..3b7334c 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,51 @@ { - "name": "sca-auth", - "version": "4.3.2", - "main": "api/auth.js", - "scripts": { - "start": "node api/auth.js", - "test": "mocha --reporter spec", - "apidoc": "apidoc -i api/ -o ui/apidoc/", - "cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha -- -R spec test/*.js" - }, - "dependencies": { - "async": "^2.4.1", - "bcryptjs": "^2.4.3", - "body-parser": "^1.17.2", - "clone": "^2.1.1", - "config": "^3.1.0", - "cookie-parser": "^1.4.3", - "cors": "^2.8.3", - "coveralls": "^2.13.1", - "express": "^4.15.3", - "express-jwt": "^5.3.0", - "express-winston": "^2.4.0", - "jsonwebtoken": "^7.4.1", - "mocha": "^3.4.1", - "node-uuid": "^1.4.8", - "nodemailer": "^4.0.1", - "optimist": "^0.6.1", - "passport": "^0.3.2", - "passport-facebook": "^2.1.1", - "passport-github2": "^0.1.11", - "passport-google-oauth20": "^1.0.0", - "passport-ldapauth": "^1.0.0", - "passport-local": "~1.0.0", - "passport-oauth2": "^1.4.0", - "promise": "^7.0.4", - "readline-sync": "^1.4.10", - "request": "^2.81.0", - "sequelize": "^3.30.4", - "sequelize-json": "^2.1.2", - "sqlite3": "^3.1.8", - "underscore": "^1.8.3", - "winston": "^2.3.0", - "xml2js": "^0.4.17" - }, - "devDependencies": { - "chai": "^3.5.0", - "coveralls": "^2.11.9", - "istanbul": "^0.4.3", - "mocha": "^3.0.2", - "supertest": "^3.0.0" - } + "name": "sca-auth", + "version": "4.3.2", + "main": "api/auth.js", + "scripts": { + "start": "node api/auth.js", + "test": "mocha --reporter spec", + "apidoc": "apidoc -i api/ -o ui/apidoc/", + "cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha -- -R spec test/*.js" + }, + "dependencies": { + "async": "^2.4.1", + "bcryptjs": "^2.4.3", + "body-parser": "^1.17.2", + "clone": "^2.1.1", + "config": "^3.1.0", + "cookie-parser": "^1.4.3", + "cors": "^2.8.3", + "express": "^4.15.3", + "express-jwt": "^5.3.0", + "express-winston": "^2.4.0", + "jsonwebtoken": "^7.4.1", + "node-uuid": "^1.4.8", + "nodemailer": "^4.0.1", + "optimist": "^0.6.1", + "passport": "^0.3.2", + "passport-facebook": "^2.1.1", + "passport-github2": "^0.1.11", + "passport-google-oauth20": "^1.0.0", + "passport-ldapauth": "^1.0.0", + "passport-local": "~1.0.0", + "passport-oauth2": "^1.4.0", + "promise": "^7.0.4", + "readline-sync": "^1.4.10", + "request": "^2.81.0", + "sequelize": "^3.30.4", + "sequelize-json": "^2.1.2", + "sqlite3": "^5.0.2", + "underscore": "^1.8.3", + "winston": "^2.3.0", + "xml2js": "^0.4.17" + }, + "devDependencies": { + "chai": "^3.5.0", + "coveralls": "^2.13.1", + "istanbul": "^0.4.3", + "mocha": "^3.4.1", + "nyc": "^15.1.0", + "supertest": "^3.0.0" + } } From 72524231906630a429fea206615359b84e699926 Mon Sep 17 00:00:00 2001 From: John Grigutis Date: Thu, 26 Aug 2021 20:34:56 +0000 Subject: [PATCH 4/6] Adds ESLint, Prettier, & eslint-config-prettier. Makes Prettier the default formatter in Visual Studio Code. Ensures files are formatted upon save. Adds additional files to be ignored by Prettier. --- .eslintrc.json | 13 ++++ .prettierignore | 149 ++++++++++++++++++++++++++++++++++++++++++ .prettierrc.json | 1 + .vscode/settings.json | 4 ++ package.json | 3 + 5 files changed, 170 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 .vscode/settings.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3833f10 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,13 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es2021": true, + "node": true + }, + "extends": ["eslint:recommended", "prettier"], + "parserOptions": { + "ecmaVersion": 12 + }, + "rules": {} +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..8ea95f8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,149 @@ +## Node.js (2021-05-25) +## https://github.com/github/gitignore/blob/master/Node.gitignore + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + + +## From mj82 (2019-06-26) + +**/barn +ui/bower_components +api/config/auth.* +api/config/*.password +api/config/index.js +api/config/*.jwt +ui/config.js +nohup*.out +debug +docker/config +docker/db +.*.swp +.*.swo + + +## From grigutis (2021-07-30) + +**/package-lock.json + + +## Additions for Prettier (2021-08-19) + +*.min.* +ui/apidoc diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..324a961 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true +} diff --git a/package.json b/package.json index 3b7334c..5895983 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,12 @@ "devDependencies": { "chai": "^3.5.0", "coveralls": "^2.13.1", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.3.0", "istanbul": "^0.4.3", "mocha": "^3.4.1", "nyc": "^15.1.0", + "prettier": "2.3.2", "supertest": "^3.0.0" } } From eec223cbf96c2407f22d22ed7863daefd57eb897 Mon Sep 17 00:00:00 2001 From: John Grigutis Date: Thu, 26 Aug 2021 20:56:49 +0000 Subject: [PATCH 5/6] Fixes html errors caught by Prettier. --- ui/t/account.html | 2 +- ui/t/signin.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/t/account.html b/ui/t/account.html index aef6eb3..11d4f6d 100644 --- a/ui/t/account.html +++ b/ui/t/account.html @@ -43,7 +43,7 @@

Change Password

- + diff --git a/ui/t/signin.html b/ui/t/signin.html index b8a4bb4..a55da52 100644 --- a/ui/t/signin.html +++ b/ui/t/signin.html @@ -73,7 +73,7 @@
- +
From 9e124f35cc3cc47e6ff8ec02a1fa9718efb310c0 Mon Sep 17 00:00:00 2001 From: John Grigutis Date: Thu, 26 Aug 2021 20:59:42 +0000 Subject: [PATCH 6/6] Formats with Prettier. --- .travis.yml | 28 +- README.md | 1 + api/auth.js | 10 +- api/common.js | 155 +++-- api/controllers/facebook.js | 209 ++++--- api/controllers/github.js | 267 +++++--- api/controllers/google.js | 227 ++++--- api/controllers/index.js | 50 +- api/controllers/iucas.js | 243 +++++--- api/controllers/ldap.js | 88 +-- api/controllers/local.js | 188 +++--- api/controllers/oidc.js | 331 ++++++---- api/controllers/orcid.js | 276 ++++---- api/controllers/root.js | 562 ++++++++++------- api/controllers/signup.js | 175 +++--- api/controllers/x509.js | 248 +++++--- api/migration.js | 140 +++-- api/models/group.js | 30 +- api/models/index.js | 58 +- api/models/migration.js | 19 +- api/models/user.js | 230 ++++--- api/server.js | 83 ++- bin/auth.js | 788 ++++++++++++----------- config/default.json | 6 +- etc/shared/auth.ui.js | 29 +- example/get_groups.js | 32 +- example/login.js | 26 +- example/query_users.js | 42 +- example/refresh.js | 28 +- example/register_key.js | 26 +- example/users.js | 34 +- test/common.js | 58 +- test/test.js | 153 +++-- ui/common.js | 2 +- ui/css/style.css | 78 +-- ui/css/style.less | 6 +- ui/index.html | 103 +-- ui/iucascb.html | 157 +++-- ui/js/app.js | 450 +++++++------ ui/js/controllers.js | 1179 +++++++++++++++++++++-------------- ui/package.json | 58 +- ui/t/account.html | 366 ++++++++--- ui/t/adminuser.html | 80 ++- ui/t/adminusers.html | 50 +- ui/t/confirm_email.html | 15 +- ui/t/footer.html | 14 +- ui/t/forgotpass.html | 60 +- ui/t/group.html | 68 +- ui/t/groups.html | 39 +- ui/t/inactive.html | 14 +- ui/t/passwordstrength.html | 20 +- ui/t/signin.html | 164 +++-- ui/t/signup.html | 63 +- ui/t/userlist.html | 6 +- 54 files changed, 4670 insertions(+), 3162 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5e0033f..53dad7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,29 @@ language: node_js compiler: - - clang - - gcc + - clang + - gcc node_js: - - 6 + - 6 install: - - npm install - - if [ "$CXX" = "g++" ]; then export CXX="g++-4.8" CC="gcc-4.8"; fi + - npm install + - if [ "$CXX" = "g++" ]; then export CXX="g++-4.8" CC="gcc-4.8"; fi addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - gcc-4.8 - - g++-4.8 - - clang + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - gcc-4.8 + - g++-4.8 + - clang script: - - ./test/install.sh && npm run cover + - ./test/install.sh && npm run cover # Send coverage data to Coveralls after_script: "cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js" notifications: - slack: research-technologies:izdZdry8NEjrDmTjbHL5dCA8 + slack: research-technologies:izdZdry8NEjrDmTjbHL5dCA8 diff --git a/README.md b/README.md index c726794..8bd6cf5 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ cd /etc/sca-auth wget https://raw.githubusercontent.com/soichih/sca-auth/master/api/config/index.js.sample cp index.js.sample index.js ``` + And edit index.js. You need to point various paths to the right place. Just contact me if you aren't sure. You also need to create public / prviate keys (see above for `openssl genrsa`) diff --git a/api/auth.js b/api/auth.js index 0420d2a..5623c22 100755 --- a/api/auth.js +++ b/api/auth.js @@ -1,10 +1,8 @@ #!/usr/bin/node -'use strict'; +"use strict"; -var server = require('./server'); -server.start(function(err) { - if(err) throw err; +var server = require("./server"); +server.start(function (err) { + if (err) throw err; console.log("service started"); }); - - diff --git a/api/common.js b/api/common.js index f6ed17c..1fa3539 100644 --- a/api/common.js +++ b/api/common.js @@ -1,19 +1,18 @@ +const fs = require("fs"); +const jwt = require("jsonwebtoken"); +const nodemailer = require("nodemailer"); +const uuid = require("node-uuid"); +const winston = require("winston"); -const fs = require('fs'); -const jwt = require('jsonwebtoken'); -const nodemailer = require('nodemailer'); -const uuid = require('node-uuid'); -const winston = require('winston'); - -const config = require('./config'); +const config = require("./config"); const logger = new winston.Logger(config.logger.winston); -exports.createClaim = function(user, cb) { +exports.createClaim = function (user, cb) { //load groups (using sequelize generated code) - user.getGroups({attributes: ['id']}).then(function(groups) { + user.getGroups({ attributes: ["id"] }).then(function (groups) { var gids = []; - groups.forEach(function(group) { - gids.push(group.id); + groups.forEach(function (group) { + gids.push(group.id); }); /* http://websec.io/2014/08/04/Securing-Requests-with-JWT.html iss: The issuer of the token @@ -26,93 +25,119 @@ exports.createClaim = function(user, cb) { */ cb(null, { - "iss": config.auth.iss, - "exp": (Date.now() + config.auth.ttl)/1000, - "iat": (Date.now())/1000, - "scopes": user.scopes, - + iss: config.auth.iss, + exp: (Date.now() + config.auth.ttl) / 1000, + iat: Date.now() / 1000, + scopes: user.scopes, + //can't use user.username which might not be set - "sub": user.id, //TODO - toString() this!? + sub: user.id, //TODO - toString() this!? - "gids": gids, - "profile": { + gids: gids, + profile: { username: user.username, email: user.email, - fullname: user.fullname + fullname: user.fullname, }, }); }); -} +}; -exports.signJwt = function(claim) { +exports.signJwt = function (claim) { return jwt.sign(claim, config.auth.private_key, config.auth.sign_opt); -} +}; function do_send_email_confirmation(url, user, cb) { - var fullurl = url+"#!/confirm_email/"+user.id+"/"+user.email_confirmation_token; + var fullurl = + url + + "#!/confirm_email/" + + user.id + + "/" + + user.email_confirmation_token; - var transporter = nodemailer.createTransport(config.local.mailer); - transporter.sendMail({ - from: config.local.email_confirmation.from, - to: user.email, - subject: config.local.email_confirmation.subject, - text: "Hello!\n\nIf you have created a new pSConfig Web Admin (PWA) account, please visit following URL to confirm your email address.\n\n"+ fullurl, - //html: ejs.render(html_template, params), - }, function(err, info) { - if(err) return cb(err); - if(info && info.response) logger.info("notification sent: "+info.response); - cb(); - }); + var transporter = nodemailer.createTransport(config.local.mailer); + transporter.sendMail( + { + from: config.local.email_confirmation.from, + to: user.email, + subject: config.local.email_confirmation.subject, + text: + "Hello!\n\nIf you have created a new pSConfig Web Admin (PWA) account, please visit following URL to confirm your email address.\n\n" + + fullurl, + //html: ejs.render(html_template, params), + }, + function (err, info) { + if (err) return cb(err); + if (info && info.response) + logger.info("notification sent: " + info.response); + cb(); + } + ); } -exports.send_email_confirmation = function(url, user, cb) { +exports.send_email_confirmation = function (url, user, cb) { //need to generate token if it's not set yet - if(!user.email_confirmation_token) { + if (!user.email_confirmation_token) { user.email_confirmation_token = uuid.v4(); - user.save().then(function() { + user.save().then(function () { do_send_email_confirmation(url, user, cb); }); } else { do_send_email_confirmation(url, user, cb); } -} +}; -exports.send_resetemail = function(url, user, cb) { - var transporter = nodemailer.createTransport(config.local.mailer); - var fullurl = url+"#!/forgotpass/"+user.password_reset_token; - transporter.sendMail({ - from: config.local.email_passreset.from, - to: user.email, - subject: config.local.email_passreset.subject, - text: "Hello!\n\nIf you have requested to reset your password, please visit "+fullurl+" to reset your password (using the same browser you've used to send the request", - }, function(err, info) { - if(err) return cb(err); - if(info && info.response) logger.info("notification sent: "+info.response); - cb(); - }); -} +exports.send_resetemail = function (url, user, cb) { + var transporter = nodemailer.createTransport(config.local.mailer); + var fullurl = url + "#!/forgotpass/" + user.password_reset_token; + transporter.sendMail( + { + from: config.local.email_passreset.from, + to: user.email, + subject: config.local.email_passreset.subject, + text: + "Hello!\n\nIf you have requested to reset your password, please visit " + + fullurl + + " to reset your password (using the same browser you've used to send the request", + }, + function (err, info) { + if (err) return cb(err); + if (info && info.response) + logger.info("notification sent: " + info.response); + cb(); + } + ); +}; //probbably deprecated -exports.setJwtCookies = function(claim, res) { +exports.setJwtCookies = function (claim, res) { var token = exports.signJwt(claim); - res.cookie('jwt', token, {domain: '.ppa.iu.edu', httpOnly: true, secure: true, path: '/'}); - res.cookie('XSRF-TOKEN', claim.xsrf, {domain: '.ppa.iu.edu', secure: true, path: '/'}); -} + res.cookie("jwt", token, { + domain: ".ppa.iu.edu", + httpOnly: true, + secure: true, + path: "/", + }); + res.cookie("XSRF-TOKEN", claim.xsrf, { + domain: ".ppa.iu.edu", + secure: true, + path: "/", + }); +}; //return scopes that exists in both o1 and o2 -exports.intersect_scopes = function(o1, o2) { +exports.intersect_scopes = function (o1, o2) { var intersect = {}; - for(var k in o1) { + for (var k in o1) { var v1 = o1[k]; - if(o2[k] === undefined) continue; //key doesn't exist in o2.. + if (o2[k] === undefined) continue; //key doesn't exist in o2.. var v2 = o2[k]; //if(typeof v1 ! = typeof v2) return; //type doesn't match var vs = []; - v1.forEach(function(v) { - if(~v2.indexOf(v)) vs.push(v); + v1.forEach(function (v) { + if (~v2.indexOf(v)) vs.push(v); }); intersect[k] = vs; } return intersect; -} - +}; diff --git a/api/controllers/facebook.js b/api/controllers/facebook.js index b801556..e45629a 100644 --- a/api/controllers/facebook.js +++ b/api/controllers/facebook.js @@ -1,105 +1,144 @@ - //contrib -var express = require('express'); +var express = require("express"); var router = express.Router(); -var request = require('request'); -var winston = require('winston'); -var jwt = require('express-jwt'); -var clone = require('clone'); -var passport = require('passport'); -var GitHubStrategy = require('passport-facebook').Strategy; +var request = require("request"); +var winston = require("winston"); +var jwt = require("express-jwt"); +var clone = require("clone"); +var passport = require("passport"); +var GitHubStrategy = require("passport-facebook").Strategy; //mine -var config = require('../config'); +var config = require("../config"); var logger = new winston.Logger(config.logger.winston); -var common = require('../common'); -var db = require('../models'); +var common = require("../common"); +var db = require("../models"); -passport.use(new GitHubStrategy({ - clientID: config.facebook.app_id, - clientSecret: config.facebook.app_secret, - callbackURL: config.facebook.callback_url, -}, function(accessToken, refreshToken, profile, cb) { - db.User.findOne({where: {"facebook": profile.id}}).then(function(user) { - cb(null, user, profile); - }); -})); +passport.use( + new GitHubStrategy( + { + clientID: config.facebook.app_id, + clientSecret: config.facebook.app_secret, + callbackURL: config.facebook.callback_url, + }, + function (accessToken, refreshToken, profile, cb) { + db.User.findOne({ where: { facebook: profile.id } }).then(function ( + user + ) { + cb(null, user, profile); + }); + } + ) +); //normal signin -router.get('/signin', passport.authenticate('facebook')); +router.get("/signin", passport.authenticate("facebook")); //callback that handles both normal and association(if cookies.associate_jwt is set and valid) -router.get('/callback', jwt({ - secret: config.auth.public_key, - credentialsRequired: false, - getToken: function(req) { return req.cookies.associate_jwt; }, -}), function(req, res, next) { - console.log("facebook signin /callback called "); - passport.authenticate('facebook', function(err, user, info) { - if(err) { - console.error(err); - return res.redirect('/auth/#!/signin?msg='+"Failed to authenticate"); - } - if(req.user) { - //association - res.clearCookie('associate_jwt'); - if(user) { - //TODO - #/settings/account doesn't handle msg yet - return res.redirect('/auth/#!/settings/account?msg=The google account is already associated to a SCA account'); +router.get( + "/callback", + jwt({ + secret: config.auth.public_key, + credentialsRequired: false, + getToken: function (req) { + return req.cookies.associate_jwt; + }, + }), + function (req, res, next) { + console.log("facebook signin /callback called "); + passport.authenticate("facebook", function (err, user, info) { + if (err) { + console.error(err); + return res.redirect( + "/auth/#!/signin?msg=" + "Failed to authenticate" + ); } - db.User.findOne({where: {id: req.user.sub}}).then(function(user) { - if(!user) throw new Error("couldn't find user record with SCA sub:"+req.user.sub); - user.facebook = info.id; - user.save().then(function() { - console.log("saved"); - console.dir(user); - console.dir(info); - res.redirect('/auth/#!/settings/account'); + if (req.user) { + //association + res.clearCookie("associate_jwt"); + if (user) { + //TODO - #/settings/account doesn't handle msg yet + return res.redirect( + "/auth/#!/settings/account?msg=The google account is already associated to a SCA account" + ); + } + db.User.findOne({ where: { id: req.user.sub } }).then(function ( + user + ) { + if (!user) + throw new Error( + "couldn't find user record with SCA sub:" + + req.user.sub + ); + user.facebook = info.id; + user.save().then(function () { + console.log("saved"); + console.dir(user); + console.dir(info); + res.redirect("/auth/#!/settings/account"); + }); }); - }); - } else { - if(!user) { - return res.redirect('/auth/#!/signin?msg='+"Your facebook account is not registered to SCA yet. Please login using your username/password first, then associate your facebook account inside account settings."); - } - common.createClaim(user, function(err, claim) { - if(err) return next(err); - var jwt = common.signJwt(claim); - user.updateTime('facebook_login'); - user.save().then(function() { - //res.json({message: "Login Success!", jwt: jwt}); - //res.set('jwt', jwt); - res.redirect('/auth/#!/success/'+jwt); + } else { + if (!user) { + return res.redirect( + "/auth/#!/signin?msg=" + + "Your facebook account is not registered to SCA yet. Please login using your username/password first, then associate your facebook account inside account settings." + ); + } + common.createClaim(user, function (err, claim) { + if (err) return next(err); + var jwt = common.signJwt(claim); + user.updateTime("facebook_login"); + user.save().then(function () { + //res.json({message: "Login Success!", jwt: jwt}); + //res.set('jwt', jwt); + res.redirect("/auth/#!/success/" + jwt); + }); }); - }); - } - })(req, res, next); -}); + } + })(req, res, next); + } +); //start facebook account association -router.get('/associate/:jwt', jwt({secret: config.auth.public_key, -getToken: function(req) { return req.params.jwt; }}), -function(req, res, next) { - res.cookie("associate_jwt", req.params.jwt, { - //it's really overkill but .. why not? (maybe helps to hide from log?) - httpOnly: true, - secure: true, - maxAge: 1000*60*5,//5 minutes should be enough - }); - passport.authenticate('facebook')(req, res, next); -}); +router.get( + "/associate/:jwt", + jwt({ + secret: config.auth.public_key, + getToken: function (req) { + return req.params.jwt; + }, + }), + function (req, res, next) { + res.cookie("associate_jwt", req.params.jwt, { + //it's really overkill but .. why not? (maybe helps to hide from log?) + httpOnly: true, + secure: true, + maxAge: 1000 * 60 * 5, //5 minutes should be enough + }); + passport.authenticate("facebook")(req, res, next); + } +); //should I refactor? -router.put('/disconnect', jwt({secret: config.auth.public_key}), function(req, res, next) { - db.User.findOne({ - where: {id: req.user.sub} - }).then(function(user) { - if(!user) res.status(401).end(); - user.facebook = null; - user.save().then(function() { - res.json({message: "Successfully disconnected facebook account.", user: user}); - }); - }); -}); +router.put( + "/disconnect", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + db.User.findOne({ + where: { id: req.user.sub }, + }).then(function (user) { + if (!user) res.status(401).end(); + user.facebook = null; + user.save().then(function () { + res.json({ + message: "Successfully disconnected facebook account.", + user: user, + }); + }); + }); + } +); module.exports = router; diff --git a/api/controllers/github.js b/api/controllers/github.js index e706fb9..c6f4a8a 100644 --- a/api/controllers/github.js +++ b/api/controllers/github.js @@ -1,110 +1,155 @@ - //contrib -const express = require('express'); +const express = require("express"); const router = express.Router(); -const request = require('request'); -const winston = require('winston'); -const jwt = require('express-jwt'); -const clone = require('clone'); -const passport = require('passport'); -const GitHubStrategy = require('passport-github2').Strategy; +const request = require("request"); +const winston = require("winston"); +const jwt = require("express-jwt"); +const clone = require("clone"); +const passport = require("passport"); +const GitHubStrategy = require("passport-github2").Strategy; //mine -const config = require('../config'); +const config = require("../config"); const logger = new winston.Logger(config.logger.winston); -const common = require('../common'); -const db = require('../models'); +const common = require("../common"); +const db = require("../models"); console.log("github common", common); console.log("github config", config); -passport.use(new GitHubStrategy({ - clientID: config.github.client_id, - clientSecret: config.github.client_secret, - callbackURL: config.github.callback_url, -}, function(accessToken, refreshToken, profile, cb) { - db.User.findOne({where: {"github": profile.username }}).then(function(user) { - cb(null, user, profile); - }); -})); +passport.use( + new GitHubStrategy( + { + clientID: config.github.client_id, + clientSecret: config.github.client_secret, + callbackURL: config.github.callback_url, + }, + function (accessToken, refreshToken, profile, cb) { + db.User.findOne({ where: { github: profile.username } }).then( + function (user) { + cb(null, user, profile); + } + ); + } + ) +); //normal signin -router.get('/signin', passport.authenticate('github')); +router.get("/signin", passport.authenticate("github")); //callback that handles both normal and association(if cookies.associate_jwt is set and valid) -router.get('/callback', jwt({ - secret: config.auth.public_key, - credentialsRequired: false, - getToken: function(req) { - return req.cookies.associate_jwt; - }, -}), function(req, res, next) { - console.log("github signin /callback called "); - passport.authenticate('github', /*{failureRedirect: '/pwa/auth/error'},*/ function(err, user, profile) { - logger.debug("github callback", JSON.stringify(profile, null, 4)); - if(err) { - console.error(err); - return res.redirect('/pwa/auth/#!/signin?msg='+"Failed to authenticate"); - } - if(req.user) { - //association - res.clearCookie('associate_jwt'); - if(user) { - var messages = [{ - type: "error", - message: "Your github account is already associated to another account. Please signoff / login with your github account." - }]; - res.cookie('messages', JSON.stringify(messages), {path: '/'}); - res.redirect('/pwa/auth/#!/settings/account'); - } else { - db.User.findOne({where: {id: req.user.sub}}).then(function(user) { - if(!user) throw new Error("couldn't find user record with SCA sub:"+req.user.sub); - user.github = profile.username; - user.save().then(function() { - var messages = [{ - type: "success", - message: "Successfully associated your github account" - }]; - res.cookie('messages', JSON.stringify(messages), {path: '/'}); - res.redirect('/pwa/auth/#!/settings/account'); - }); - }); - } - } else { - if(!user) { - if(config.github.auto_register) { - register_newuser(profile, res, next); +router.get( + "/callback", + jwt({ + secret: config.auth.public_key, + credentialsRequired: false, + getToken: function (req) { + return req.cookies.associate_jwt; + }, + }), + function (req, res, next) { + console.log("github signin /callback called "); + passport.authenticate( + "github", + /*{failureRedirect: '/pwa/auth/error'},*/ function ( + err, + user, + profile + ) { + logger.debug( + "github callback", + JSON.stringify(profile, null, 4) + ); + if (err) { + console.error(err); + return res.redirect( + "/pwa/auth/#!/signin?msg=" + "Failed to authenticate" + ); + } + if (req.user) { + //association + res.clearCookie("associate_jwt"); + if (user) { + var messages = [ + { + type: "error", + message: + "Your github account is already associated to another account. Please signoff / login with your github account.", + }, + ]; + res.cookie("messages", JSON.stringify(messages), { + path: "/", + }); + res.redirect("/pwa/auth/#!/settings/account"); + } else { + db.User.findOne({ where: { id: req.user.sub } }).then( + function (user) { + if (!user) + throw new Error( + "couldn't find user record with SCA sub:" + + req.user.sub + ); + user.github = profile.username; + user.save().then(function () { + var messages = [ + { + type: "success", + message: + "Successfully associated your github account", + }, + ]; + res.cookie( + "messages", + JSON.stringify(messages), + { path: "/" } + ); + res.redirect( + "/pwa/auth/#!/settings/account" + ); + }); + } + ); + } } else { - res.redirect('/pwa/auth/#!/signin?msg='+"Your github account is not yet registered. Please login using your username/password first, then associate your github account inside account settings."); + if (!user) { + if (config.github.auto_register) { + register_newuser(profile, res, next); + } else { + res.redirect( + "/pwa/auth/#!/signin?msg=" + + "Your github account is not yet registered. Please login using your username/password first, then associate your github account inside account settings." + ); + } + } else { + issue_jwt(user, profile, function (err, jwt) { + if (err) return next(err); + res.redirect("/pwa/auth/#!/success/" + jwt); + }); + } } - } else { - issue_jwt(user, profile, function(err, jwt) { - if(err) return next(err); - res.redirect('/pwa/auth/#!/success/'+jwt); - }); } - } - })(req, res, next); -}); + )(req, res, next); + } +); function register_newuser(profile, res, next) { var u = clone(config.auth.default); //u.username = profile.username; - + //email could collide with already existing account - let signup take care of this //u.email = profile.emails[0].value; //TODO always set? //u.email_confirmed = true; //let's trust github u.github = profile.username; u.fullname = profile.displayName; - db.User.create(u).then(function(user) { + db.User.create(u).then(function (user) { logger.info("registered new user", JSON.stringify(user)); - user.addMemberGroups(u.gids, function() { - issue_jwt(user, profile, function(err, jwt) { - if(err) return next(err); + user.addMemberGroups(u.gids, function () { + issue_jwt(user, profile, function (err, jwt) { + if (err) return next(err); logger.info("registration success", jwt); - res.redirect('/pwa/auth/#!/signup/'+jwt); + res.redirect("/pwa/auth/#!/signup/" + jwt); //res.redirect('/pwa/auth/#!/success/'+jwt); }); }); @@ -112,40 +157,54 @@ function register_newuser(profile, res, next) { } function issue_jwt(user, profile, cb) { - common.createClaim(user, function(err, claim) { - if(err) return cb(err); + common.createClaim(user, function (err, claim) { + if (err) return cb(err); var jwt = common.signJwt(claim); - user.updateTime('github_login'); - user.save().then(function() { + user.updateTime("github_login"); + user.save().then(function () { cb(null, jwt); }); }); } - //start github account association -router.get('/associate/:jwt', jwt({secret: config.auth.public_key, getToken: function(req) { return req.params.jwt; }}), -function(req, res, next) { - res.cookie("associate_jwt", req.params.jwt, { - //it's really overkill but .. why not? (maybe helps to hide from log?) - httpOnly: true, - secure: true, - maxAge: 1000*60*5,//5 minutes should be enough - }); - passport.authenticate('github')(req, res, next); -}); +router.get( + "/associate/:jwt", + jwt({ + secret: config.auth.public_key, + getToken: function (req) { + return req.params.jwt; + }, + }), + function (req, res, next) { + res.cookie("associate_jwt", req.params.jwt, { + //it's really overkill but .. why not? (maybe helps to hide from log?) + httpOnly: true, + secure: true, + maxAge: 1000 * 60 * 5, //5 minutes should be enough + }); + passport.authenticate("github")(req, res, next); + } +); //should I refactor? -router.put('/disconnect', jwt({secret: config.auth.public_key}), function(req, res, next) { - db.User.findOne({ - where: {id: req.user.sub} - }).then(function(user) { - if(!user) res.status(401).end(); - user.github = null; - user.save().then(function() { - res.json({message: "Successfully disconnected github account.", user: user}); - }); - }); -}); +router.put( + "/disconnect", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + db.User.findOne({ + where: { id: req.user.sub }, + }).then(function (user) { + if (!user) res.status(401).end(); + user.github = null; + user.save().then(function () { + res.json({ + message: "Successfully disconnected github account.", + user: user, + }); + }); + }); + } +); module.exports = router; diff --git a/api/controllers/google.js b/api/controllers/google.js index 0100e41..3285818 100644 --- a/api/controllers/google.js +++ b/api/controllers/google.js @@ -1,32 +1,38 @@ - //contrib -var express = require('express'); +var express = require("express"); var router = express.Router(); -var request = require('request'); -var winston = require('winston'); -var jwt = require('express-jwt'); -var clone = require('clone'); -var passport = require('passport'); -var GoogleStrategy = require('passport-google-oauth20').Strategy; +var request = require("request"); +var winston = require("winston"); +var jwt = require("express-jwt"); +var clone = require("clone"); +var passport = require("passport"); +var GoogleStrategy = require("passport-google-oauth20").Strategy; //mine -var config = require('../config'); +var config = require("../config"); var logger = new winston.Logger(config.logger.winston); -var common = require('../common'); -var db = require('../models'); +var common = require("../common"); +var db = require("../models"); -passport.use(new GoogleStrategy({ - clientID: config.google.client_id, - clientSecret: config.google.client_secret, - callbackURL: config.google.callback_url, -}, function(accessToken, refreshToken, profile, cb) { - //console.log("authenticated with google"); - //console.dir(profile); - db.User.findOne({where: {"googleid": profile.id }}).then(function(user) { - cb(null, user, profile); - }); -})); +passport.use( + new GoogleStrategy( + { + clientID: config.google.client_id, + clientSecret: config.google.client_secret, + callbackURL: config.google.callback_url, + }, + function (accessToken, refreshToken, profile, cb) { + //console.log("authenticated with google"); + //console.dir(profile); + db.User.findOne({ where: { googleid: profile.id } }).then(function ( + user + ) { + cb(null, user, profile); + }); + } + ) +); /* profile sample 1|sca-auth | _json: @@ -99,73 +105,99 @@ function issue_jwt(user, cb) { */ //normal signin -router.get('/signin', passport.authenticate('google', {scope: ['profile']})); +router.get("/signin", passport.authenticate("google", { scope: ["profile"] })); //callback that handles both normal and association(if cookies.associate_jwt is set and valid) -router.get('/callback', jwt({ - secret: config.auth.public_key, - credentialsRequired: false, - getToken: function(req) { - return req.cookies.associate_jwt; - }, -}), function(req, res, next) { - console.log("google signin /callback called "); - passport.authenticate('google', function(err, user, info) { - if(err) { - console.error(err); - return res.redirect('/auth/#!/signin?msg='+"Failed to authenticate"); - } - if(req.user) { - //association - res.clearCookie('associate_jwt'); - if(user) { - //TODO - #/settings/account doesn't handle msg yet - return res.redirect('/auth/#!/settings/account?msg=The github account is already associated to a SCA account'); +router.get( + "/callback", + jwt({ + secret: config.auth.public_key, + credentialsRequired: false, + getToken: function (req) { + return req.cookies.associate_jwt; + }, + }), + function (req, res, next) { + console.log("google signin /callback called "); + passport.authenticate("google", function (err, user, info) { + if (err) { + console.error(err); + return res.redirect( + "/auth/#!/signin?msg=" + "Failed to authenticate" + ); } - db.User.findOne({where: {id: req.user.sub}}).then(function(user) { - if(!user) throw new Error("couldn't find user record with SCA sub:"+req.user.sub); - user.googleid = info.id; - user.save().then(function() { - //console.log("saved"); - //console.dir(user); - res.redirect('/auth/#!/settings/account'); + if (req.user) { + //association + res.clearCookie("associate_jwt"); + if (user) { + //TODO - #/settings/account doesn't handle msg yet + return res.redirect( + "/auth/#!/settings/account?msg=The github account is already associated to a SCA account" + ); + } + db.User.findOne({ where: { id: req.user.sub } }).then(function ( + user + ) { + if (!user) + throw new Error( + "couldn't find user record with SCA sub:" + + req.user.sub + ); + user.googleid = info.id; + user.save().then(function () { + //console.log("saved"); + //console.dir(user); + res.redirect("/auth/#!/settings/account"); + }); }); - }); - } else { - //normal sign in - if(!user) { - return res.redirect('/auth/#!/signin?msg='+"Your google account is not registered to SCA yet. Please login using your username/password first, then associate your google account inside account settings."); - } - common.createClaim(user, function(err, claim) { - if(err) return next(err); - var jwt = common.signJwt(claim); - user.updateTime('google_login'); - user.save().then(function() { - //res.json({message: "Login Success!", jwt: jwt}); - //res.set('jwt', jwt); - res.redirect('/auth/#!/success/'+jwt); + } else { + //normal sign in + if (!user) { + return res.redirect( + "/auth/#!/signin?msg=" + + "Your google account is not registered to SCA yet. Please login using your username/password first, then associate your google account inside account settings." + ); + } + common.createClaim(user, function (err, claim) { + if (err) return next(err); + var jwt = common.signJwt(claim); + user.updateTime("google_login"); + user.save().then(function () { + //res.json({message: "Login Success!", jwt: jwt}); + //res.set('jwt', jwt); + res.redirect("/auth/#!/success/" + jwt); + }); }); - }); - } - })(req, res, next); -}); + } + })(req, res, next); + } +); //start account association -router.get('/associate/:jwt', jwt({secret: config.auth.public_key, -getToken: function(req) { return req.params.jwt; }}), -function(req, res, next) { - //TODO - maybe I should create a temporarly token ? - //var callbackurl = "https://soichi7.ppa.iu.edu/api/auth/google/callback/associate/"+req.params.jwt; - //var exp = new Date(); - //exp.setMinutes(exp.getMinutes()+5); //only valid for 5 minutes (and will be removed once account is associated) - res.cookie("associate_jwt", req.params.jwt, { - //it's really overkill but .. why not? (maybe helps to hide from log?) - httpOnly: true, - secure: true, - maxAge: 1000*60*5,//5 minutes should be enough - //expires: exp, - }); - passport.authenticate('google', { scope: ['profile'], /*callbackURL: callbackurl*/ })(req, res, next); -}); +router.get( + "/associate/:jwt", + jwt({ + secret: config.auth.public_key, + getToken: function (req) { + return req.params.jwt; + }, + }), + function (req, res, next) { + //TODO - maybe I should create a temporarly token ? + //var callbackurl = "https://soichi7.ppa.iu.edu/api/auth/google/callback/associate/"+req.params.jwt; + //var exp = new Date(); + //exp.setMinutes(exp.getMinutes()+5); //only valid for 5 minutes (and will be removed once account is associated) + res.cookie("associate_jwt", req.params.jwt, { + //it's really overkill but .. why not? (maybe helps to hide from log?) + httpOnly: true, + secure: true, + maxAge: 1000 * 60 * 5, //5 minutes should be enough + //expires: exp, + }); + passport.authenticate("google", { + scope: ["profile"] /*callbackURL: callbackurl*/, + })(req, res, next); + } +); /* router.get('/callback/associate/:jwt', jwt({secret: config.auth.public_key, @@ -191,16 +223,23 @@ function(req, res, next) { */ //should I refactor? -router.put('/disconnect', jwt({secret: config.auth.public_key}), function(req, res, next) { - db.User.findOne({ - where: {id: req.user.sub} - }).then(function(user) { - if(!user) res.status(401).end(); - user.googleid = null; - user.save().then(function() { - res.json({message: "Successfully disconnected google account.", user: user}); - }); - }); -}); +router.put( + "/disconnect", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + db.User.findOne({ + where: { id: req.user.sub }, + }).then(function (user) { + if (!user) res.status(401).end(); + user.googleid = null; + user.save().then(function () { + res.json({ + message: "Successfully disconnected google account.", + user: user, + }); + }); + }); + } +); module.exports = router; diff --git a/api/controllers/index.js b/api/controllers/index.js index d1f50a9..55cc9cd 100644 --- a/api/controllers/index.js +++ b/api/controllers/index.js @@ -1,45 +1,45 @@ -'use strict'; +"use strict"; //contrib -var express = require('express'); +var express = require("express"); var router = express.Router(); -var jwt = require('express-jwt'); +var jwt = require("express-jwt"); //mine -const config = require('../config'); +const config = require("../config"); //router.use('/pwa/', require('./root')); -router.use('/', require('./root')); +router.use("/", require("./root")); -if(config.auth.allow_signup !== false) { - router.use('/signup', require('./signup')); +if (config.auth.allow_signup !== false) { + router.use("/signup", require("./signup")); } -if(config.local) { - router.use('/local', require('./local')); +if (config.local) { + router.use("/local", require("./local")); } -if(config.ldap) { - router.use('/ldap', require('./ldap')); +if (config.ldap) { + router.use("/ldap", require("./ldap")); } -if(config.iucas) { - router.use('/iucas', require('./iucas')); +if (config.iucas) { + router.use("/iucas", require("./iucas")); } -if(config.x509) { - router.use('/x509', require('./x509')); +if (config.x509) { + router.use("/x509", require("./x509")); } -if(config.github) { - router.use('/github', require('./github')); +if (config.github) { + router.use("/github", require("./github")); } -if(config.google) { - router.use('/google', require('./google')); +if (config.google) { + router.use("/google", require("./google")); } -if(config.facebook) { - router.use('/facebook', require('./facebook')); +if (config.facebook) { + router.use("/facebook", require("./facebook")); } -if(config.oidc) { - router.use('/oidc', require('./oidc')); +if (config.oidc) { + router.use("/oidc", require("./oidc")); } -if(config.orcid) { - router.use('/orcid', require('./orcid')); +if (config.orcid) { + router.use("/orcid", require("./orcid")); } module.exports = router; diff --git a/api/controllers/iucas.js b/api/controllers/iucas.js index 1aa3737..251be01 100644 --- a/api/controllers/iucas.js +++ b/api/controllers/iucas.js @@ -1,36 +1,42 @@ - //contrib -var express = require('express'); +var express = require("express"); var router = express.Router(); -var request = require('request'); -var winston = require('winston'); -var jwt = require('express-jwt'); -var clone = require('clone'); +var request = require("request"); +var winston = require("winston"); +var jwt = require("express-jwt"); +var clone = require("clone"); //mine -const config = require('../config'); +const config = require("../config"); var logger = new winston.Logger(config.logger.winston); -var common = require('../common'); -var db = require('../models'); +var common = require("../common"); +var db = require("../models"); function finduserByiucasid(id, cb) { - db.User.findOne({where: {"iucas": id}}).then(function(user) { + db.User.findOne({ where: { iucas: id } }).then(function (user) { cb(null, user); }); } //TODO - maybe I should refactor this? function associate(jwt, uid, res, cb) { - logger.info("associating user with iucas id:"+uid); - db.User.findOne({where: {id: jwt.sub}}).then(function(user) { - if(!user) return cb("couldn't find user record with SCA sub:"+jwt.sub); + logger.info("associating user with iucas id:" + uid); + db.User.findOne({ where: { id: jwt.sub } }).then(function (user) { + if (!user) + return cb("couldn't find user record with SCA sub:" + jwt.sub); user.iucas = uid; - user.save().then(function() { - var messages = [{type: "success", /*title: "IUCAS ID Associated",*/ message: "We have associated IU ID:"+uid+" to your account"}]; - res.cookie('messages', JSON.stringify(messages), {path: '/'}); - issue_jwt(user, function(err, jwt) { - if(err) return cb(err); + user.save().then(function () { + var messages = [ + { + type: "success", + /*title: "IUCAS ID Associated",*/ message: + "We have associated IU ID:" + uid + " to your account", + }, + ]; + res.cookie("messages", JSON.stringify(messages), { path: "/" }); + issue_jwt(user, function (err, jwt) { + if (err) return cb(err); cb(null, jwt); }); }); @@ -38,29 +44,35 @@ function associate(jwt, uid, res, cb) { } function register_newuser(uid, res, next) { - logger.info("registering new user with iucas id:"+uid); - db.User.findOne({where: {'username': uid}}).then(function(user) { - if(user) { - logger.warn("username already registered:"+uid+"(can't auto register)"); + logger.info("registering new user with iucas id:" + uid); + db.User.findOne({ where: { username: uid } }).then(function (user) { + if (user) { + logger.warn( + "username already registered:" + uid + "(can't auto register)" + ); //TODO - instead of showing this error message, maybe I should redirect user to - //a page to force user to login via user/pass, then associate the IU CAS IU once user logs in - next("This is the first time you login with IU CAS account, "+ - "but we couldn't register this account since the username '"+uid+"' is already registered in our system. "+ - "If you have already registered with username / password, please login with username / password first, "); + //a page to force user to login via user/pass, then associate the IU CAS IU once user logs in + next( + "This is the first time you login with IU CAS account, " + + "but we couldn't register this account since the username '" + + uid + + "' is already registered in our system. " + + "If you have already registered with username / password, please login with username / password first, " + ); } else { //brand new user - go ahead and create a new account using IU id as sca user id var u = clone(config.auth.default); u.username = uid; //let's use IU id as local username - u.email = uid+"@iu.edu"; + u.email = uid + "@iu.edu"; u.email_confirmed = true; //let's trust IU.. u.iucas = uid; //TODO I should refactor this part somehow.. - db.User.create(u).then(function(user) { - user.addMemberGroups(u.gids, function() { - //done(user); - issue_jwt(user, function(err, jwt) { - if(err) return next(err); - res.json({jwt:jwt, registered: true}); + db.User.create(u).then(function (user) { + user.addMemberGroups(u.gids, function () { + //done(user); + issue_jwt(user, function (err, jwt) { + if (err) return next(err); + res.json({ jwt: jwt, registered: true }); }); }); }); @@ -69,10 +81,10 @@ function register_newuser(uid, res, next) { } function issue_jwt(user, cb) { - user.updateTime('iucas_login'); - user.save().then(function() { - common.createClaim(user, function(err, claim) { - if(err) return cb(err); + user.updateTime("iucas_login"); + user.save().then(function () { + common.createClaim(user, function (err, claim) { + if (err) return cb(err); var jwt = common.signJwt(claim); cb(null, jwt); }); @@ -90,73 +102,104 @@ router.get('/lock', function(req, res, next) { }); */ -router.get('/verify', jwt({secret: config.auth.public_key, credentialsRequired: false}), function(req, res, next) { - var ticket = req.query.casticket; +router.get( + "/verify", + jwt({ secret: config.auth.public_key, credentialsRequired: false }), + function (req, res, next) { + var ticket = req.query.casticket; - //guess casurl using referer - TODO - should I use cookie and pass it from the UI method begin_iucas() instead? - //var casurl = config.iucas.home_url; - if(!req.headers.referer) return next("Referer not set in header.."); - casurl = req.headers.referer; - request({ - url: 'https://cas.iu.edu/cas/validate?cassvc=IU&casticket='+ticket+'&casurl='+casurl, - timeout: 1000*5, //long enough? - }, function (err, response, body) { - if(err) return next(err); - if (response.statusCode == 200) { - var reslines = body.split("\n"); - if(reslines[0].trim() == "yes") { - var uid = reslines[1].trim(); - finduserByiucasid(uid, function(err, user) { - if(err) return next(err); - if(!user) { - if(req.user) { - //If user is already logged in, but no iucas associated yet.. then auto-associate. - //If someone with only local account let someone else login via iucas on the same browser, while the first person is logged in, - //that someone else can then start using the first person's account after he leaves the computer. However, user intentionally - //visiting /auth page when the first user is already logged into a system is very unlikely, since the user most likely will - //sign out so that second user can login. also, if this situation to ever occur, user should be presented with - //"we have associated your account" message so that first user should be aware of this happening - associate(req.user, uid, res, function(err, jwt) { - res.json({jwt: jwt}); - }); - } else if(config.iucas.auto_register) { - register_newuser(uid, res, next); - } else { - res.redirect('/auth/#!/signin?msg='+"Your IU account("+profile.sub+") is not yet registered. Please login using your username/password first, then associate your IU account inside the account settings."); - } - } else { - var err = user.check(); - if(err) return next(err); - - //all good. issue token - logger.debug("iucas authentication successful. iu id:"+uid); - issue_jwt(user, function(err, jwt) { - if(err) return next(err); - res.json({jwt:jwt}); + //guess casurl using referer - TODO - should I use cookie and pass it from the UI method begin_iucas() instead? + //var casurl = config.iucas.home_url; + if (!req.headers.referer) return next("Referer not set in header.."); + casurl = req.headers.referer; + request( + { + url: + "https://cas.iu.edu/cas/validate?cassvc=IU&casticket=" + + ticket + + "&casurl=" + + casurl, + timeout: 1000 * 5, //long enough? + }, + function (err, response, body) { + if (err) return next(err); + if (response.statusCode == 200) { + var reslines = body.split("\n"); + if (reslines[0].trim() == "yes") { + var uid = reslines[1].trim(); + finduserByiucasid(uid, function (err, user) { + if (err) return next(err); + if (!user) { + if (req.user) { + //If user is already logged in, but no iucas associated yet.. then auto-associate. + //If someone with only local account let someone else login via iucas on the same browser, while the first person is logged in, + //that someone else can then start using the first person's account after he leaves the computer. However, user intentionally + //visiting /auth page when the first user is already logged into a system is very unlikely, since the user most likely will + //sign out so that second user can login. also, if this situation to ever occur, user should be presented with + //"we have associated your account" message so that first user should be aware of this happening + associate( + req.user, + uid, + res, + function (err, jwt) { + res.json({ jwt: jwt }); + } + ); + } else if (config.iucas.auto_register) { + register_newuser(uid, res, next); + } else { + res.redirect( + "/auth/#!/signin?msg=" + + "Your IU account(" + + profile.sub + + ") is not yet registered. Please login using your username/password first, then associate your IU account inside the account settings." + ); + } + } else { + var err = user.check(); + if (err) return next(err); + + //all good. issue token + logger.debug( + "iucas authentication successful. iu id:" + + uid + ); + issue_jwt(user, function (err, jwt) { + if (err) return next(err); + res.json({ jwt: jwt }); + }); + } }); + } else { + logger.error("IUCAS failed to validate"); + res.sendStatus("403"); //Is 403:Forbidden appropriate return code? } - }); - } else { - logger.error("IUCAS failed to validate"); - res.sendStatus("403");//Is 403:Forbidden appropriate return code? + } else { + //non 200 code... + next(body); + } } - } else { - //non 200 code... - next(body); - } - }) -}); + ); + } +); -router.put('/disconnect', jwt({secret: config.auth.public_key}), function(req, res, next) { - db.User.findOne({ - where: {id: req.user.sub} - }).then(function(user) { - if(!user) res.status(401).end(); - user.iucas = null; - user.save().then(function() { - res.json({message: "Successfully disconnected IUCAS account.", user: user}); - }); - }); -}); +router.put( + "/disconnect", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + db.User.findOne({ + where: { id: req.user.sub }, + }).then(function (user) { + if (!user) res.status(401).end(); + user.iucas = null; + user.save().then(function () { + res.json({ + message: "Successfully disconnected IUCAS account.", + user: user, + }); + }); + }); + } +); module.exports = router; diff --git a/api/controllers/ldap.js b/api/controllers/ldap.js index 262e715..21308ec 100644 --- a/api/controllers/ldap.js +++ b/api/controllers/ldap.js @@ -1,55 +1,61 @@ - //contrib -var express = require('express'); +var express = require("express"); var router = express.Router(); -var passport = require('passport'); -var passportldap = require('passport-ldapauth'); -var winston = require('winston'); -var jwt = require('express-jwt'); -var clone = require('clone'); +var passport = require("passport"); +var passportldap = require("passport-ldapauth"); +var winston = require("winston"); +var jwt = require("express-jwt"); +var clone = require("clone"); //mine -var config = require('../config'); +var config = require("../config"); var logger = new winston.Logger(config.logger.winston); -var common = require('../common'); -var db = require('../models'); +var common = require("../common"); +var db = require("../models"); function registerUser(ldapuser, cb) { var u = clone(config.auth.default); - u.ldap = ldapuser.cn; + u.ldap = ldapuser.cn; u.username = ldapuser.cn; u.iucas = ldapuser.cn; //TODO should I do this? u.email = ldapuser.mail; u.email_confirmed = true; //let's trust IU - u.fullname = ldapuser.givenName+" "+ldapuser.sn; + u.fullname = ldapuser.givenName + " " + ldapuser.sn; var user = db.User.build(u); - logger.info("registering user through first time ldap auth - ldap.cn:"+u.username); + logger.info( + "registering user through first time ldap auth - ldap.cn:" + u.username + ); //logger.info(user); - user.save().then(function() { + user.save().then(function () { cb(null, user); }); } -passport.use(new passportldap(config.ldap, - function(ldapuser, done) { +passport.use( + new passportldap(config.ldap, function (ldapuser, done) { logger.info("handling ldap auth post processing"); - //look for ldap field, but also look for username (for now) so that users who registered with + //look for ldap field, but also look for username (for now) so that users who registered with //local username/pass can be authenticated //TODO - this means IU user can *takeover* non-IU SCA user.. eventually I might allow both //local and ldap authentication, but it will be very confusing to the user. - db.User.findOne({where: {$or: {"ldap": ldapuser.cn, "username": ldapuser.cn }}}).then(function(user) { + db.User.findOne({ + where: { $or: { ldap: ldapuser.cn, username: ldapuser.cn } }, + }).then(function (user) { if (!user) { //first time(?) .. auto register //TODO - need to handle username collision registerUser(ldapuser, done); } else { - if(!user.ldap) { - //if user is matched using username, and ldap is empty, populate ldap so that - //user will be matched with ldap next time .. + if (!user.ldap) { + //if user is matched using username, and ldap is empty, populate ldap so that + //user will be matched with ldap next time .. //eventually I should make username matching optional or completely drop it.. - logger.warn("user account matched via username but ldap was empty - setting ldap account:"+ldapuser.cn); + logger.warn( + "user account matched via username but ldap was empty - setting ldap account:" + + ldapuser.cn + ); user.ldap = ldapuser.cn; - user.save().then(function() { + user.save().then(function () { done(null, user); }); } else { @@ -57,8 +63,8 @@ passport.use(new passportldap(config.ldap, } } }); - } -)); + }) +); /** * @api {post} /ldap/auth Perform ldap authentication @@ -71,21 +77,25 @@ passport.use(new passportldap(config.ldap, * * @apiSuccess {Object} jwt JWT token */ -router.post('/auth', function(req, res, next) { - passport.authenticate('ldapauth', {session: false}, function(err, user, info) { - if (err) return next(err); - if (!user) return res.status(404).json(info); - var err = user.check(); - if(err) return next(err); - common.createClaim(user, function(err, claim) { - if(err) return next(err); - var jwt = common.signJwt(claim); - user.updateTime('ldap_login'); - user.save().then(function() { - res.json({message: "Login Success!", jwt: jwt}); +router.post("/auth", function (req, res, next) { + passport.authenticate( + "ldapauth", + { session: false }, + function (err, user, info) { + if (err) return next(err); + if (!user) return res.status(404).json(info); + var err = user.check(); + if (err) return next(err); + common.createClaim(user, function (err, claim) { + if (err) return next(err); + var jwt = common.signJwt(claim); + user.updateTime("ldap_login"); + user.save().then(function () { + res.json({ message: "Login Success!", jwt: jwt }); + }); }); - }); - })(req, res, next); + } + )(req, res, next); }); module.exports = router; diff --git a/api/controllers/local.js b/api/controllers/local.js index 7ed955c..62f9fea 100644 --- a/api/controllers/local.js +++ b/api/controllers/local.js @@ -1,41 +1,47 @@ - //contrib -const express = require('express'); +const express = require("express"); const router = express.Router(); -const passport = require('passport'); -const passport_localst = require('passport-local').Strategy; -const winston = require('winston'); -const jwt = require('express-jwt'); +const passport = require("passport"); +const passport_localst = require("passport-local").Strategy; +const winston = require("winston"); +const jwt = require("express-jwt"); //mine -const config = require('../config'); +const config = require("../config"); const logger = new winston.Logger(config.logger.winston); -const common = require('../common'); -const db = require('../models'); +const common = require("../common"); +const db = require("../models"); -passport.use(new passport_localst( - function(username, password, done) { - db.User.findOne({where: {$or: {"username": username, "email": username }}}).then(function(user) { +passport.use( + new passport_localst(function (username, password, done) { + db.User.findOne({ + where: { $or: { username: username, email: username } }, + }).then(function (user) { if (!user) { - return done(null, false, { message: 'Incorrect email or username' }); + return done(null, false, { + message: "Incorrect email or username", + }); } else { var err = user.check(); - if(err) return done(null, false, err); - if(!user.password_hash) { - return done(null, false, { message: 'Password login is not enabled for this account' }); + if (err) return done(null, false, err); + if (!user.password_hash) { + return done(null, false, { + message: + "Password login is not enabled for this account", + }); } - if(!user.isPassword(password)) { + if (!user.isPassword(password)) { //delay returning to defend against password sweeping attack - setTimeout(function() { - done(null, false, { message: 'Incorrect password' }); + setTimeout(function () { + done(null, false, { message: "Incorrect password" }); }, 2000); return; } done(null, user); } }); - } -)); + }) +); /** * @api {post} /local/auth Perform authentication @@ -48,52 +54,59 @@ passport.use(new passport_localst( * * @apiSuccess {Object} jwt JWT token */ -router.post('/auth', function(req, res, next) { - passport.authenticate('local', function(err, user, info) { +router.post("/auth", function (req, res, next) { + passport.authenticate("local", function (err, user, info) { if (err) return next(err); if (!user) return next(info); - common.createClaim(user, function(err, claim) { - if(err) return next(err); + common.createClaim(user, function (err, claim) { + if (err) return next(err); var jwt = common.signJwt(claim); - user.updateTime('local_login'); - user.save().then(function() { - res.json({message: "Login Success!", jwt: jwt}); + user.updateTime("local_login"); + user.save().then(function () { + res.json({ message: "Login Success!", jwt: jwt }); }); }); })(req, res, next); }); //used to setpassword if password_hash is empty or update exiting password (with a valid current password) -router.put('/setpass', jwt({secret: config.auth.public_key}), function(req, res, next) { - db.User.findOne({where: {id: req.user.sub}}).then(function(user) { - logger.debug("setting password for sub:"+req.user.sub); - if(user) { - if(user.password_hash) { - if(!user.isPassword(req.body.password_old)) { - return setTimeout(function() { - next("Wrong current password"); - }, 2000); +router.put( + "/setpass", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + db.User.findOne({ where: { id: req.user.sub } }).then(function (user) { + logger.debug("setting password for sub:" + req.user.sub); + if (user) { + if (user.password_hash) { + if (!user.isPassword(req.body.password_old)) { + return setTimeout(function () { + next("Wrong current password"); + }, 2000); + } } - } - user.setPassword(req.body.password, function(err) { - if(err) return next(err); - user.updateTime('password_reset'); - user.save().then(function() { - res.json({status: "ok", message: "Password reset successfully."}); + user.setPassword(req.body.password, function (err) { + if (err) return next(err); + user.updateTime("password_reset"); + user.save().then(function () { + res.json({ + status: "ok", + message: "Password reset successfully.", + }); + }); }); - }); - } else { - logger.info("failed to find user with sub:"+req.user.sub); - res.status(404).end(); - } - }); -}); + } else { + logger.info("failed to find user with sub:" + req.user.sub); + res.status(404).end(); + } + }); + } +); /** * @api {post} /local/resetpass Handle both resetpass request and fulfillment request * @apiName LocalAuth * @apiDescription (mode 1) - * When this API is called with email field, it will create reset token associated with the owner of the email address + * When this API is called with email field, it will create reset token associated with the owner of the email address * and send reset request email with the token on the URL. While doing so, it sets httpOnly cookie with random string * to be stored on user's browser. * (mode 2) @@ -109,39 +122,63 @@ router.put('/setpass', jwt({secret: config.auth.public_key}), function(req, res, * * @apiSuccess {Object} message Containing success message */ -router.post('/resetpass', function(req, res, next) { - if(req.body.email) { +router.post("/resetpass", function (req, res, next) { + if (req.body.email) { //initiate password reset var email = req.body.email; - db.User.findOne({where: {email: email}}).then(function(user) { - if(!user) return res.status(404).json({message: "No such email registered"}); - //we need 2 tokens - 1 to confirm user, and 1 to match the browser (cookie) - user.password_reset_token = Math.random().toString(36).substr(2); - user.password_reset_cookie = Math.random().toString(36).substr(2); - common.send_resetemail(req.headers.referer||config.local.url, user, function(err) { - if(err) return next(err); - user.save().then(function() { - res.cookie('password_reset', user.password_reset_cookie, {httpOnly: true, secure: true}); //should be default to session cookie - res.json({message: "Reset token sent"}); - }); - }); - - }).catch(next); + db.User.findOne({ where: { email: email } }) + .then(function (user) { + if (!user) + return res + .status(404) + .json({ message: "No such email registered" }); + //we need 2 tokens - 1 to confirm user, and 1 to match the browser (cookie) + user.password_reset_token = Math.random() + .toString(36) + .substr(2); + user.password_reset_cookie = Math.random() + .toString(36) + .substr(2); + common.send_resetemail( + req.headers.referer || config.local.url, + user, + function (err) { + if (err) return next(err); + user.save().then(function () { + res.cookie( + "password_reset", + user.password_reset_cookie, + { httpOnly: true, secure: true } + ); //should be default to session cookie + res.json({ message: "Reset token sent" }); + }); + } + ); + }) + .catch(next); } else { //fulfull password reset var token = req.body.token; var password = req.body.password; var cookie = req.cookies.password_reset; - if(!token || !password) return next("missing parameters"); + if (!token || !password) return next("missing parameters"); //TODO should I apply minimum password length? - db.User.findOne({where: {password_reset_token: token, password_reset_cookie: cookie}}).then(function(user) { - if(user) { - user.setPassword(password, function(err) { - if(err) return next(err); + db.User.findOne({ + where: { + password_reset_token: token, + password_reset_cookie: cookie, + }, + }).then(function (user) { + if (user) { + user.setPassword(password, function (err) { + if (err) return next(err); user.password_reset_token = null; user.password_reset_cookie = null; - user.save().then(function() { - res.json({status: "ok", message: "Password reset successfully."}); + user.save().then(function () { + res.json({ + status: "ok", + message: "Password reset successfully.", + }); }); }); } else return next("couldn't find the token provided"); @@ -150,7 +187,6 @@ router.post('/resetpass', function(req, res, next) { }); //reset password (with a valid reset token) ?token=123 -router.put('/resetpass', function(req, res, next) { -}); +router.put("/resetpass", function (req, res, next) {}); module.exports = router; diff --git a/api/controllers/oidc.js b/api/controllers/oidc.js index 5890e95..2adf4e3 100644 --- a/api/controllers/oidc.js +++ b/api/controllers/oidc.js @@ -1,21 +1,20 @@ - //contrib -const express = require('express'); +const express = require("express"); const router = express.Router(); -const request = require('request'); -const winston = require('winston'); -const jwt = require('express-jwt'); -const clone = require('clone'); -const passport = require('passport'); -const OAuth2Strategy = require('passport-oauth2').Strategy; -const xml2js = require('xml2js'); +const request = require("request"); +const winston = require("winston"); +const jwt = require("express-jwt"); +const clone = require("clone"); +const passport = require("passport"); +const OAuth2Strategy = require("passport-oauth2").Strategy; +const xml2js = require("xml2js"); //mine -const config = require('../config'); +const config = require("../config"); const logger = new winston.Logger(config.logger.winston); -const common = require('../common'); -const db = require('../models'); +const common = require("../common"); +const db = require("../models"); //cache idp list /* @@ -28,112 +27,155 @@ const db = require('../models'); */ var cache_idps = null; -request.get({url: config.oidc.idplist}, (err, res, xml)=>{ - if(err) throw err; - xml2js.parseString(xml, (err, list)=>{ - if(err) throw err; +request.get({ url: config.oidc.idplist }, (err, res, xml) => { + if (err) throw err; + xml2js.parseString(xml, (err, list) => { + if (err) throw err; cache_idps = list.idps.idp; }); }); -const oidc_strat = new OAuth2Strategy({ - authorizationURL: config.oidc.authorization_url, - tokenURL: config.oidc.token_url, - clientID: config.oidc.client_id, - clientSecret: config.oidc.client_secret, - callbackURL: config.oidc.callback_url, - scope: "openid profile email org.cilogon.userinfo", -}, function(accessToken, refreshToken, profile, cb) { - - //cilogon doesn't set profile.. I need to make another call to fetch the info - logger.debug("oidc loading userinfo ..", accessToken, profile); - request.get({url: config.oidc.userinfo_url, qs: {access_token: accessToken}, json: true}, function(err, _res, profile) { - if(err) return cb(err); - db.User.findOne({where: {"oidc_subs": {$like: "%\""+profile.sub+"\"%"}}}).then(function(user) { - cb(null, user, profile); - }); - }); -}); +const oidc_strat = new OAuth2Strategy( + { + authorizationURL: config.oidc.authorization_url, + tokenURL: config.oidc.token_url, + clientID: config.oidc.client_id, + clientSecret: config.oidc.client_secret, + callbackURL: config.oidc.callback_url, + scope: "openid profile email org.cilogon.userinfo", + }, + function (accessToken, refreshToken, profile, cb) { + //cilogon doesn't set profile.. I need to make another call to fetch the info + logger.debug("oidc loading userinfo ..", accessToken, profile); + request.get( + { + url: config.oidc.userinfo_url, + qs: { access_token: accessToken }, + json: true, + }, + function (err, _res, profile) { + if (err) return cb(err); + db.User.findOne({ + where: { oidc_subs: { $like: '%"' + profile.sub + '"%' } }, + }).then(function (user) { + cb(null, user, profile); + }); + } + ); + } +); oidc_strat.name = "oauth2-oidc"; passport.use(oidc_strat); //initiate oauth2 login! -OAuth2Strategy.prototype.authorizationParams = function(options) { - return { selected_idp: options.idp } -} -router.get('/signin', function(req, res, next) { +OAuth2Strategy.prototype.authorizationParams = function (options) { + return { selected_idp: options.idp }; +}; +router.get("/signin", function (req, res, next) { logger.debug("oidc signin commencing"); passport.authenticate(oidc_strat.name, { //this will be used by my authorizationParams() and selected_idp will be injected to authorized url - idp: req.query.idp + idp: req.query.idp, })(req, res, next); }); function find_profile(profiles, sub) { var idx = -1; - profiles.forEach(function(profile, x) { - if(profile.sub == sub) idx = x; + profiles.forEach(function (profile, x) { + if (profile.sub == sub) idx = x; }); return idx; } //this handles both normal callback from incommon and account association (if cookies.associate_jwt is set) -router.get('/callback', -jwt({ secret: config.auth.public_key, credentialsRequired: false, getToken: req=>req.cookies.associate_jwt }), -function(req, res, next) { - passport.authenticate(oidc_strat.name, function(err, user, profile) { - logger.debug("oidc callback", profile); - if(err) { - console.error(err); - return res.redirect('/auth/#!/signin?msg='+"Failed to authenticate oidc"); - } - if(req.user) { - //logged in via associate_jwt.. - logger.info("handling oidc association"); - res.clearCookie('associate_jwt'); - if(user) { - //SUB is already registered to another account.. - //TODO - should I let user *steal* the OIDC sub from another account? - var messages = [{ - type: "error", - message: "There is another account with the same OIDC ID registered. Please contact support." - }]; - res.cookie('messages', JSON.stringify(messages), {path: '/'}); - res.redirect('/auth/#!/settings/account'); - } else { - db.User.findOne({where: {id: req.user.sub}}).then(function(user) { - if(!user) throw new Error("couldn't find user record with sub:"+req.user.sub); - var subs = user.get('oidc_subs'); - if(!subs) subs = []; - if(!~find_profile(subs, profile.sub)) subs.push(profile); - user.set('oidc_subs', subs); - user.save().then(function() { - var messages = [{ - type: "success", - message: "Successfully associated your OIDC account" - }]; - res.cookie('messages', JSON.stringify(messages), {path: '/'}); - res.redirect('/auth/#!/settings/account'); - }); - }); +router.get( + "/callback", + jwt({ + secret: config.auth.public_key, + credentialsRequired: false, + getToken: (req) => req.cookies.associate_jwt, + }), + function (req, res, next) { + passport.authenticate(oidc_strat.name, function (err, user, profile) { + logger.debug("oidc callback", profile); + if (err) { + console.error(err); + return res.redirect( + "/auth/#!/signin?msg=" + "Failed to authenticate oidc" + ); } - } else { - logger.info("handling oidc callback"); - if(!user) { - if(config.oidc.auto_register) { - register_newuser(profile, res, next); + if (req.user) { + //logged in via associate_jwt.. + logger.info("handling oidc association"); + res.clearCookie("associate_jwt"); + if (user) { + //SUB is already registered to another account.. + //TODO - should I let user *steal* the OIDC sub from another account? + var messages = [ + { + type: "error", + message: + "There is another account with the same OIDC ID registered. Please contact support.", + }, + ]; + res.cookie("messages", JSON.stringify(messages), { + path: "/", + }); + res.redirect("/auth/#!/settings/account"); } else { - res.redirect('/auth/#!/signin?msg='+"Your InCommon account("+profile.sub+") is not yet registered. Please login using your username/password first, then associate your InCommon account inside the account settings."); + db.User.findOne({ where: { id: req.user.sub } }).then( + function (user) { + if (!user) + throw new Error( + "couldn't find user record with sub:" + + req.user.sub + ); + var subs = user.get("oidc_subs"); + if (!subs) subs = []; + if (!~find_profile(subs, profile.sub)) + subs.push(profile); + user.set("oidc_subs", subs); + user.save().then(function () { + var messages = [ + { + type: "success", + message: + "Successfully associated your OIDC account", + }, + ]; + res.cookie( + "messages", + JSON.stringify(messages), + { path: "/" } + ); + res.redirect("/auth/#!/settings/account"); + }); + } + ); } } else { - issue_jwt(user, profile, function(err, jwt) { - if(err) return next(err); - res.redirect('/auth/#!/success/'+jwt); - }); - } - } - })(req, res, next); -}); + logger.info("handling oidc callback"); + if (!user) { + if (config.oidc.auto_register) { + register_newuser(profile, res, next); + } else { + res.redirect( + "/auth/#!/signin?msg=" + + "Your InCommon account(" + + profile.sub + + ") is not yet registered. Please login using your username/password first, then associate your InCommon account inside the account settings." + ); + } + } else { + issue_jwt(user, profile, function (err, jwt) { + if (err) return next(err); + res.redirect("/auth/#!/success/" + jwt); + }); + } + } + })(req, res, next); + } +); function register_newuser(profile, res, next) { var u = clone(config.auth.default); @@ -145,73 +187,88 @@ function register_newuser(profile, res, next) { //u.email_confirmed = true; //let's trust InCommon u.oidc_subs = [profile]; - u.fullname = profile.given_name+" "+profile.family_name; - db.User.create(u).then(function(user) { + u.fullname = profile.given_name + " " + profile.family_name; + db.User.create(u).then(function (user) { logger.info("registered new user", JSON.stringify(user)); - user.addMemberGroups(u.gids, function() { - issue_jwt(user, profile, function(err, jwt) { - if(err) return next(err); + user.addMemberGroups(u.gids, function () { + issue_jwt(user, profile, function (err, jwt) { + if (err) return next(err); logger.info("registration success", jwt); - res.redirect('/auth/#!/signup/'+jwt); + res.redirect("/auth/#!/signup/" + jwt); }); }); }); } function issue_jwt(user, profile, cb) { - common.createClaim(user, function(err, claim) { - if(err) return cb(err); + common.createClaim(user, function (err, claim) { + if (err) return cb(err); var jwt = common.signJwt(claim); - user.updateTime('oidc_login:'+profile.sub); - user.save().then(function() { + user.updateTime("oidc_login:" + profile.sub); + user.save().then(function () { cb(null, jwt); }); }); } //start oidc account association -router.get('/associate/:jwt', jwt({secret: config.auth.public_key, getToken: req=>req.params.jwt}), -function(req, res, next) { - res.cookie("associate_jwt", req.params.jwt, { - //it's really overkill but .. why not? (maybe helps to hide from log?) - httpOnly: true, - secure: true, - maxAge: 1000*60*5,//5 minutes should be enough - }); - passport.authenticate(oidc_strat.name)(req, res, next); -}); +router.get( + "/associate/:jwt", + jwt({ secret: config.auth.public_key, getToken: (req) => req.params.jwt }), + function (req, res, next) { + res.cookie("associate_jwt", req.params.jwt, { + //it's really overkill but .. why not? (maybe helps to hide from log?) + httpOnly: true, + secure: true, + maxAge: 1000 * 60 * 5, //5 minutes should be enough + }); + passport.authenticate(oidc_strat.name)(req, res, next); + } +); //should I refactor? -router.put('/disconnect', jwt({secret: config.auth.public_key}), function(req, res, next) { - var sub = req.body.sub; - db.User.findOne({ - where: {id: req.user.sub} - }).then(function(user) { - if(!user) res.status(401).end(); - var subs = user.get('oidc_subs'); - var pos = find_profile(subs, sub); - if(~pos) subs.splice(pos, 1); - user.set('oidc_subs', subs); - user.save().then(function() { - logger.debug(user.toString()); - res.json({message: "Successfully disconnected an OIDC account", user: user}); - }); - }); -}); +router.put( + "/disconnect", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + var sub = req.body.sub; + db.User.findOne({ + where: { id: req.user.sub }, + }).then(function (user) { + if (!user) res.status(401).end(); + var subs = user.get("oidc_subs"); + var pos = find_profile(subs, sub); + if (~pos) subs.splice(pos, 1); + user.set("oidc_subs", subs); + user.save().then(function () { + logger.debug(user.toString()); + res.json({ + message: "Successfully disconnected an OIDC account", + user: user, + }); + }); + }); + } +); //query idp -router.get('/idp', function(req, res, next) { - if(!cache_idps) return next("idp list not yet loaded"); +router.get("/idp", function (req, res, next) { + if (!cache_idps) return next("idp list not yet loaded"); var query = req.query.q; - if(!query) return next("no query"); - if(query) query = query.toLowerCase(); + if (!query) return next("no query"); + if (query) query = query.toLowerCase(); logger.debug(req.params); var idps = []; - cache_idps.forEach(function(idp) { + cache_idps.forEach(function (idp) { var match = false; - if(idp.Organization_name && ~idp.Organization_Name[0].toLowerCase().indexOf(query)) match = true; - if(idp.Home_Page && ~idp.Home_Page[0].toLowerCase().indexOf(query)) match = true; - if(match) { + if ( + idp.Organization_name && + ~idp.Organization_Name[0].toLowerCase().indexOf(query) + ) + match = true; + if (idp.Home_Page && ~idp.Home_Page[0].toLowerCase().indexOf(query)) + match = true; + if (match) { idps.push({ idp: idp.$.entityID, org: idp.Organization_Name[0], diff --git a/api/controllers/orcid.js b/api/controllers/orcid.js index 7b33dc8..762d420 100644 --- a/api/controllers/orcid.js +++ b/api/controllers/orcid.js @@ -1,36 +1,44 @@ - //contrib -const express = require('express'); +const express = require("express"); const router = express.Router(); -const request = require('request'); -const winston = require('winston'); -const jwt = require('express-jwt'); -const clone = require('clone'); -const passport = require('passport'); -const OAuth2Strategy = require('passport-oauth2').Strategy; -const xml2js = require('xml2js'); +const request = require("request"); +const winston = require("winston"); +const jwt = require("express-jwt"); +const clone = require("clone"); +const passport = require("passport"); +const OAuth2Strategy = require("passport-oauth2").Strategy; +const xml2js = require("xml2js"); //mine -const config = require('../config'); +const config = require("../config"); const logger = new winston.Logger(config.logger.winston); -const common = require('../common'); -const db = require('../models'); +const common = require("../common"); +const db = require("../models"); -const orcid_strat = new OAuth2Strategy({ - authorizationURL: config.orcid.authorization_url, - tokenURL: config.orcid.token_url, - clientID: config.orcid.client_id, - clientSecret: config.orcid.client_secret, - callbackURL: config.orcid.callback_url, - scope: "/authenticate", -}, function(accessToken, refreshToken, profile, _needed, cb) { - logger.debug("orcid loading userinfo ..", accessToken, refreshToken, profile); - db.User.findOne({where: {"orcid": profile.orcid}}).then(function(user) { - cb(null, user, profile); - }); +const orcid_strat = new OAuth2Strategy( + { + authorizationURL: config.orcid.authorization_url, + tokenURL: config.orcid.token_url, + clientID: config.orcid.client_id, + clientSecret: config.orcid.client_secret, + callbackURL: config.orcid.callback_url, + scope: "/authenticate", + }, + function (accessToken, refreshToken, profile, _needed, cb) { + logger.debug( + "orcid loading userinfo ..", + accessToken, + refreshToken, + profile + ); + db.User.findOne({ where: { orcid: profile.orcid } }).then(function ( + user + ) { + cb(null, user, profile); + }); - /* In ORCID, email address is private by default.. user need to explicitly release it so we can get it + /* In ORCID, email address is private by default.. user need to explicitly release it so we can get it * which makes ORCID/email unreliable.. so let's forget it and ask user during signup stage //get public record //http://members.orcid.org/api/tutorial/read-orcid-records @@ -40,136 +48,178 @@ const orcid_strat = new OAuth2Strategy({ console.log("body---------------------", body); }); */ -}); + } +); orcid_strat.name = "oauth2-orcid"; passport.use(orcid_strat); //initiate oauth2 login! -OAuth2Strategy.prototype.authorizationParams = function(options) { - return { selected_idp: options.idp } -} -router.get('/signin', function(req, res, next) { +OAuth2Strategy.prototype.authorizationParams = function (options) { + return { selected_idp: options.idp }; +}; +router.get("/signin", function (req, res, next) { passport.authenticate(orcid_strat.name, { //this will be used by my authorizationParams() and selected_idp will be injected to authorized url - idp: req.query.idp + idp: req.query.idp, })(req, res, next); }); function find_profile(profiles, sub) { var idx = -1; - profiles.forEach(function(profile, x) { - if(profile.sub == sub) idx = x; + profiles.forEach(function (profile, x) { + if (profile.sub == sub) idx = x; }); return idx; } //this handles both normal callback from incommon and account association (if cookies.associate_jwt is set) -router.get('/callback', -jwt({ secret: config.auth.public_key, credentialsRequired: false, getToken: req=>req.cookies.associate_jwt }), -function(req, res, next) { - passport.authenticate(orcid_strat.name, function(err, user, profile) { - logger.debug("orcid callback", profile); - if(err) { - console.error(err); - return res.redirect('/auth/#!/signin?msg='+"Failed to authenticate orcid"); - } - if(req.user) { - //logged in via associate_jwt.. - logger.info("handling orcid association"); - res.clearCookie('associate_jwt'); - if(user) { - //SUB is already registered to another account.. - //TODO - should I let user *steal* the OIDC sub from another account? - var messages = [{ - type: "error", - message: "There is another account with the same OIDC ID registered. Please contact support." - }]; - res.cookie('messages', JSON.stringify(messages), {path: '/'}); - res.redirect('/auth/#!/settings/account'); - } else { - db.User.findOne({where: {id: req.user.sub}}).then(function(user) { - if(!user) throw new Error("couldn't find user record with sub:"+req.user.sub); - user.orcid = profile.orcid; - user.save().then(function() { - var messages = [{ - type: "success", - message: "Successfully associated your OIDC account" - }]; - res.cookie('messages', JSON.stringify(messages), {path: '/'}); - res.redirect('/auth/#!/settings/account'); - }); - }); +router.get( + "/callback", + jwt({ + secret: config.auth.public_key, + credentialsRequired: false, + getToken: (req) => req.cookies.associate_jwt, + }), + function (req, res, next) { + passport.authenticate(orcid_strat.name, function (err, user, profile) { + logger.debug("orcid callback", profile); + if (err) { + console.error(err); + return res.redirect( + "/auth/#!/signin?msg=" + "Failed to authenticate orcid" + ); } - } else { - logger.info("handling orcid callback"); - if(!user) { - if(config.orcid.auto_register) { - register_newuser(profile, res, next); + if (req.user) { + //logged in via associate_jwt.. + logger.info("handling orcid association"); + res.clearCookie("associate_jwt"); + if (user) { + //SUB is already registered to another account.. + //TODO - should I let user *steal* the OIDC sub from another account? + var messages = [ + { + type: "error", + message: + "There is another account with the same OIDC ID registered. Please contact support.", + }, + ]; + res.cookie("messages", JSON.stringify(messages), { + path: "/", + }); + res.redirect("/auth/#!/settings/account"); } else { - res.redirect('/auth/#!/signin?msg='+"Your InCommon account("+profile.sub+") is not yet registered. Please login using your username/password first, then associate your InCommon account inside the account settings."); + db.User.findOne({ where: { id: req.user.sub } }).then( + function (user) { + if (!user) + throw new Error( + "couldn't find user record with sub:" + + req.user.sub + ); + user.orcid = profile.orcid; + user.save().then(function () { + var messages = [ + { + type: "success", + message: + "Successfully associated your OIDC account", + }, + ]; + res.cookie( + "messages", + JSON.stringify(messages), + { path: "/" } + ); + res.redirect("/auth/#!/settings/account"); + }); + } + ); } } else { - issue_jwt(user, profile, function(err, jwt) { - if(err) return next(err); - res.redirect('/auth/#!/success/'+jwt); - }); - } - } - })(req, res, next); -}); + logger.info("handling orcid callback"); + if (!user) { + if (config.orcid.auto_register) { + register_newuser(profile, res, next); + } else { + res.redirect( + "/auth/#!/signin?msg=" + + "Your InCommon account(" + + profile.sub + + ") is not yet registered. Please login using your username/password first, then associate your InCommon account inside the account settings." + ); + } + } else { + issue_jwt(user, profile, function (err, jwt) { + if (err) return next(err); + res.redirect("/auth/#!/success/" + jwt); + }); + } + } + })(req, res, next); + } +); function register_newuser(profile, res, next) { var u = clone(config.auth.default); //u.username = ...; //will this break our system? u.orcid = profile.orcid; u.fullname = profile.name; - db.User.create(u).then(function(user) { + db.User.create(u).then(function (user) { logger.info("registered new user", JSON.stringify(user)); - user.addMemberGroups(u.gids, function() { - issue_jwt(user, profile, function(err, jwt) { - if(err) return next(err); + user.addMemberGroups(u.gids, function () { + issue_jwt(user, profile, function (err, jwt) { + if (err) return next(err); logger.info("registration success", jwt); - res.redirect('/auth/#!/signup/'+jwt); + res.redirect("/auth/#!/signup/" + jwt); }); }); }); } function issue_jwt(user, profile, cb) { - common.createClaim(user, function(err, claim) { - if(err) return cb(err); + common.createClaim(user, function (err, claim) { + if (err) return cb(err); var jwt = common.signJwt(claim); - user.updateTime('orcid_login'); - user.save().then(function() { + user.updateTime("orcid_login"); + user.save().then(function () { cb(null, jwt); }); }); } //start orcid account association -router.get('/associate/:jwt', jwt({secret: config.auth.public_key, getToken: req=>req.params.jwt}), -function(req, res, next) { - res.cookie("associate_jwt", req.params.jwt, { - //it's really overkill but .. why not? (maybe helps to hide from log?) - httpOnly: true, - secure: true, - maxAge: 1000*60*5,//5 minutes should be enough - }); - passport.authenticate(orcid_strat.name)(req, res, next); -}); +router.get( + "/associate/:jwt", + jwt({ secret: config.auth.public_key, getToken: (req) => req.params.jwt }), + function (req, res, next) { + res.cookie("associate_jwt", req.params.jwt, { + //it's really overkill but .. why not? (maybe helps to hide from log?) + httpOnly: true, + secure: true, + maxAge: 1000 * 60 * 5, //5 minutes should be enough + }); + passport.authenticate(orcid_strat.name)(req, res, next); + } +); //should I refactor? -router.put('/disconnect', jwt({secret: config.auth.public_key}), function(req, res, next) { - var sub = req.body.sub; - db.User.findOne({ - where: {id: req.user.sub} - }).then(function(user) { - if(!user) res.status(401).end(); - user.orcid = null; - user.save().then(function() { - res.json({message: "Successfully disconnected an ORCID account", user: user}); - }); - }); -}); +router.put( + "/disconnect", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + var sub = req.body.sub; + db.User.findOne({ + where: { id: req.user.sub }, + }).then(function (user) { + if (!user) res.status(401).end(); + user.orcid = null; + user.save().then(function () { + res.json({ + message: "Successfully disconnected an ORCID account", + user: user, + }); + }); + }); + } +); module.exports = router; diff --git a/api/controllers/root.js b/api/controllers/root.js index 61d3e12..6b2a645 100644 --- a/api/controllers/root.js +++ b/api/controllers/root.js @@ -1,24 +1,24 @@ -'use strict'; +"use strict"; -const express = require('express'); +const express = require("express"); const router = express.Router(); -const passport = require('passport'); -const winston = require('winston'); -const jwt = require('express-jwt'); -const async = require('async'); +const passport = require("passport"); +const winston = require("winston"); +const jwt = require("express-jwt"); +const async = require("async"); //mine -const config = require('../config'); +const config = require("../config"); const logger = new winston.Logger(config.logger.winston); -const common = require('../common'); -const db = require('../models'); +const common = require("../common"); +const db = require("../models"); /** * @api {post} /refresh Refresh JWT Token. - * @apiDescription + * @apiDescription * JWT Token normally lasts for a few hours. Application should call this API periodically - * to get it refreshed before it expires. - * You can also use this API to temporarily drop certain privileges you previously had to + * to get it refreshed before it expires. + * You can also use this API to temporarily drop certain privileges you previously had to * simulate user with less privileges, or make your token more secure by removing unnecessary * privileges (set scopes parameters) * @@ -30,43 +30,65 @@ const db = require('../models'); * * @apiSuccess {Object} jwt New JWT token */ -router.post('/refresh', jwt({secret: config.auth.public_key}), function(req, res, next) { - db.User.findOne({where: {id: req.user.sub}}).then(function(user) { - if(!user) return next("Couldn't find any user with sub:"+req.user.sub); - var err = user.check(); - if(err) return next(err); +router.post( + "/refresh", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + db.User.findOne({ where: { id: req.user.sub } }).then(function (user) { + if (!user) + return next("Couldn't find any user with sub:" + req.user.sub); + var err = user.check(); + if (err) return next(err); - //intersect requested scopes - if(req.body.scopes) user.scopes = common.intersect_scopes(user.scoppes, req.body.scopes); + //intersect requested scopes + if (req.body.scopes) + user.scopes = common.intersect_scopes( + user.scoppes, + req.body.scopes + ); - common.createClaim(user, function(err, claim) { - if(err) return next(err); - var jwt = common.signJwt(claim); - return res.json({jwt: jwt}); + common.createClaim(user, function (err, claim) { + if (err) return next(err); + var jwt = common.signJwt(claim); + return res.json({ jwt: jwt }); + }); }); - }); -}); + } +); //TODO this API send any SCA user email with URL provided by an user - which is a major security risk //I should use configured URL for referer -router.post('/send_email_confirmation', function(req, res, next) { - db.User.findOne({where: {id: req.body.sub}}).then(function(user) { - if(!user) return next("Couldn't find any user with sub:"+req.body.sub); - if(user.email_confirmed) return next("Email already confirmed."); - if(!req.headers.referer) return next("referer not set.. can't send confirmation"); - common.send_email_confirmation(req.headers.referer, user, function(err) { - if(err) return next(err); - res.json({message: 'Sent confirmation email with subject: '+config.local.email_confirmation.subject}); - }); +router.post("/send_email_confirmation", function (req, res, next) { + db.User.findOne({ where: { id: req.body.sub } }).then(function (user) { + if (!user) + return next("Couldn't find any user with sub:" + req.body.sub); + if (user.email_confirmed) return next("Email already confirmed."); + if (!req.headers.referer) + return next("referer not set.. can't send confirmation"); + common.send_email_confirmation( + req.headers.referer, + user, + function (err) { + if (err) return next(err); + res.json({ + message: + "Sent confirmation email with subject: " + + config.local.email_confirmation.subject, + }); + } + ); }); }); -router.post('/confirm_email', function(req, res, next) { - db.User.findOne({where: {email_confirmation_token: req.body.token}}).then(function(user) { - if(!user) return next("Couldn't find any user with token:"+req.body.token); - if(user.email_confirmed) return next("Email already confirmed."); +router.post("/confirm_email", function (req, res, next) { + db.User.findOne({ + where: { email_confirmation_token: req.body.token }, + }).then(function (user) { + if (!user) + return next("Couldn't find any user with token:" + req.body.token); + if (user.email_confirmed) return next("Email already confirmed."); user.email_confirmed = true; - user.save().then(function() { - res.json({message: "Email address confirmed! Please re-login."}); + user.save().then(function () { + res.json({ message: "Email address confirmed! Please re-login." }); }); }); }); @@ -79,10 +101,10 @@ router.post('/confirm_email', function(req, res, next) { * * @apiSuccess {String} status 'ok' or 'failed' */ -router.get('/health', function(req, res) { +router.get("/health", function (req, res) { res.json({ - status: 'ok', - headers: req.headers, + status: "ok", + headers: req.headers, }); }); @@ -124,7 +146,7 @@ router.get('/config', function(req, res) { * password_hash will be set to true if the password is set, otherwise null * * @apiGroup User - * + * * @apiHeader {String} authorization A valid JWT token "Bearer: xxxxx" * @apiSuccessExample {json} Success-Response: * HTTP/1.1 200 OK @@ -136,40 +158,57 @@ router.get('/config', function(req, res) { * "iucas": "hayashis" * } */ -router.get('/me', jwt({secret: config.auth.public_key}), function(req, res, next) { - db.User.findOne({ - where: {id: req.user.sub}, - //password_hash is replace by true/false right below - //attributes: ['username', 'fullname', 'email', 'email_confirmed', 'iucas', 'googleid', 'github', 'x509dns', 'times', 'password_hash'], - }).then(function(user) { - if(!user) return res.status(404).end(); - if(user.password_hash) user.password_hash = true; - res.json(user); - }); -}); +router.get( + "/me", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + db.User.findOne({ + where: { id: req.user.sub }, + //password_hash is replace by true/false right below + //attributes: ['username', 'fullname', 'email', 'email_confirmed', 'iucas', 'googleid', 'github', 'x509dns', 'times', 'password_hash'], + }).then(function (user) { + if (!user) return res.status(404).end(); + if (user.password_hash) user.password_hash = true; + res.json(user); + }); + } +); //return list of all users (minus password) admin only - used by user admin -router.get('/users', jwt({secret: config.auth.public_key}), function(req, res, next) { - var where = {}; - if(req.query.where) where = JSON.parse(req.query.where); - if(!~req.user.scopes.sca.indexOf("admin")) return res.send(401); - db.User.findAll({ - where: where, - //raw: true, //so that I can add _gids - - //password_hash is replaced by true/false right below - attributes: [ - 'id', 'username', 'fullname', 'password_hash', - 'email', 'email_confirmed', 'iucas', 'googleid', 'github', 'x509dns', - 'times', 'scopes', 'active'], - }).then(function(users) { +router.get( + "/users", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + var where = {}; + if (req.query.where) where = JSON.parse(req.query.where); + if (!~req.user.scopes.sca.indexOf("admin")) return res.send(401); + db.User.findAll({ + where: where, + //raw: true, //so that I can add _gids - //mask password! - users.forEach(function(user) { - if(user.password_hash) user.password_hash = true; - }); - - /* + //password_hash is replaced by true/false right below + attributes: [ + "id", + "username", + "fullname", + "password_hash", + "email", + "email_confirmed", + "iucas", + "googleid", + "github", + "x509dns", + "times", + "scopes", + "active", + ], + }).then(function (users) { + //mask password! + users.forEach(function (user) { + if (user.password_hash) user.password_hash = true; + }); + + /* //load gids for each users if(req.query.gids) { var _users = JSON.stringif(users); @@ -191,9 +230,10 @@ router.get('/users', jwt({secret: config.auth.public_key}), function(req, res, n res.json(users); } */ - res.json(users); - }); -}); + res.json(users); + }); + } +); /** * @api {get} /user/groups/:id Get list of group IDS that user is member of @@ -201,137 +241,207 @@ router.get('/users', jwt({secret: config.auth.public_key}), function(req, res, n * @apiDescription Only for admin * * @apiGroup User - * + * * @apiHeader {String} authorization A valid JWT token "Bearer: xxxxx" * @apiSuccessExample {json} Success-Response: * HTTP/1.1 200 OK - * [ 1,2,3 ] + * [ 1,2,3 ] */ -router.get('/user/groups/:id', jwt({secret: config.auth.public_key}), function(req, res, next) { - if(!~req.user.scopes.sca.indexOf("admin")) return res.send(401); - db.User.findOne({ - where: {id: req.params.id}, - }).then(function(user) { - if(!user) return res.status(404).end(); - user.getGroups({attributes: ['id']}).then(function(groups) { - var gids = []; - groups.forEach(function(group) { - gids.push(group.id); +router.get( + "/user/groups/:id", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + if (!~req.user.scopes.sca.indexOf("admin")) return res.send(401); + db.User.findOne({ + where: { id: req.params.id }, + }).then(function (user) { + if (!user) return res.status(404).end(); + user.getGroups({ attributes: ["id"] }).then(function (groups) { + var gids = []; + groups.forEach(function (group) { + gids.push(group.id); + }); + res.json(gids); }); - res.json(gids); }); - }); -}); - + } +); + //DEPRECATED - use /users //return detail from just one user - admin only (somewhat redundant from /users ??) -router.get('/user/:id', jwt({secret: config.auth.public_key}), function(req, res) { - if(!~req.user.scopes.sca.indexOf("admin")) return res.send(401); - db.User.findOne({ - where: {id: req.params.id}, - attributes: [ - 'id', 'username', 'fullname', - 'email', 'email_confirmed', 'iucas', 'googleid', 'github', 'x509dns', - 'times', 'scopes', 'active'], - }).then(function(user) { - res.json(user); - }); -}); +router.get( + "/user/:id", + jwt({ secret: config.auth.public_key }), + function (req, res) { + if (!~req.user.scopes.sca.indexOf("admin")) return res.send(401); + db.User.findOne({ + where: { id: req.params.id }, + attributes: [ + "id", + "username", + "fullname", + "email", + "email_confirmed", + "iucas", + "googleid", + "github", + "x509dns", + "times", + "scopes", + "active", + ], + }).then(function (user) { + res.json(user); + }); + } +); //update user info (admin only) -router.put('/user/:id', jwt({secret: config.auth.public_key}), function(req, res, next) { - if(!~req.user.scopes.sca.indexOf("admin")) return res.send(401); - db.User.findOne({where: {id: req.params.id}}).then(function(user) { - if (!user) return next("can't find user id:"+req.params.id); - user.update(req.body).then(function(err) { - res.json({message: "User updated successfully"}); +router.put( + "/user/:id", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + if (!~req.user.scopes.sca.indexOf("admin")) return res.send(401); + db.User.findOne({ where: { id: req.params.id } }).then(function (user) { + if (!user) return next("can't find user id:" + req.params.id); + user.update(req.body).then(function (err) { + res.json({ message: "User updated successfully" }); + }); }); - }); -}); + } +); //return list of all groups (open to all users) -router.get('/groups', jwt({secret: config.auth.public_key}), function(req, res) { - //if(!~req.user.scopes.sca.indexOf("admin")) return res.send(401); - db.Group.findAll({ - include: [ - {model: db.User, as: 'Admins', attributes: ['id', 'email', 'fullname']}, - {model: db.User, as: 'Members', attributes: ['id', 'email', 'fullname']}, - ], - //raw: true, - }).then(function(_groups) { - var groups = JSON.parse(JSON.stringify(_groups)); - groups.forEach(function(group) { - group.canedit = ~req.user.scopes.sca.indexOf("admin"); - group.Admins.forEach(function(admin) { - if(admin.id == req.user.sub) { - group.canedit = true; - } - }); +router.get( + "/groups", + jwt({ secret: config.auth.public_key }), + function (req, res) { + //if(!~req.user.scopes.sca.indexOf("admin")) return res.send(401); + db.Group.findAll({ + include: [ + { + model: db.User, + as: "Admins", + attributes: ["id", "email", "fullname"], + }, + { + model: db.User, + as: "Members", + attributes: ["id", "email", "fullname"], + }, + ], + //raw: true, + }).then(function (_groups) { + var groups = JSON.parse(JSON.stringify(_groups)); + groups.forEach(function (group) { + group.canedit = ~req.user.scopes.sca.indexOf("admin"); + group.Admins.forEach(function (admin) { + if (admin.id == req.user.sub) { + group.canedit = true; + } + }); + }); + res.json(groups); }); - res.json(groups); - }); -}); + } +); //update group (sca adimn, or admin of the group can update) -router.put('/group/:id', jwt({secret: config.auth.public_key}), function(req, res, next) { - //console.dir(req.body); - db.Group.findOne({where: {id: req.params.id}}).then(function(group) { - if (!group) return next("can't find group id:"+req.params.id); - //first I need to get current admins.. - group.getAdmins().then(function(admins) { - //console.log(req.user.scopes.sca.indexOf("admin")); - var admin_ids = []; - admins.forEach(function(admin) { - admin_ids.push(admin.id); //toString so that I can compare with indexOf - }); - //console.dir(req.user.sub); - //console.dir(admin_ids); - //console.log(req.user.sub); - //console.log(admin_ids.indexOf(req.user.sub)); - if(!~req.user.scopes.sca.indexOf("admin") && !~admin_ids.indexOf(req.user.sub)) return res.send(401); - //then update everything - group.update(req.body.group).then(function(err) { - group.setAdmins(req.body.admins).then(function() { - group.setMembers(req.body.members).then(function() { - res.json({message: "Group updated successfully"}); - }); +router.put( + "/group/:id", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + //console.dir(req.body); + db.Group.findOne({ where: { id: req.params.id } }).then(function ( + group + ) { + if (!group) return next("can't find group id:" + req.params.id); + //first I need to get current admins.. + group.getAdmins().then(function (admins) { + //console.log(req.user.scopes.sca.indexOf("admin")); + var admin_ids = []; + admins.forEach(function (admin) { + admin_ids.push(admin.id); //toString so that I can compare with indexOf }); - }).catch(function(err) { - next(err); + //console.dir(req.user.sub); + //console.dir(admin_ids); + //console.log(req.user.sub); + //console.log(admin_ids.indexOf(req.user.sub)); + if ( + !~req.user.scopes.sca.indexOf("admin") && + !~admin_ids.indexOf(req.user.sub) + ) + return res.send(401); + //then update everything + group + .update(req.body.group) + .then(function (err) { + group.setAdmins(req.body.admins).then(function () { + group + .setMembers(req.body.members) + .then(function () { + res.json({ + message: "Group updated successfully", + }); + }); + }); + }) + .catch(function (err) { + next(err); + }); }); }); - }); -}); + } +); //create new group (any user can create group?) -router.post('/group', jwt({secret: config.auth.public_key}), function(req, res, next) { - //if(!~req.user.scopes.sca.indexOf("admin")) return res.send(401); - var group = db.Group.build(req.body.group); - group.save().then(function() { - group.setAdmins(req.body.admins).then(function() { - group.setMembers(req.body.members).then(function() { - res.json({message: "Group created", group: group}); +router.post( + "/group", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + //if(!~req.user.scopes.sca.indexOf("admin")) return res.send(401); + var group = db.Group.build(req.body.group); + group + .save() + .then(function () { + group.setAdmins(req.body.admins).then(function () { + group.setMembers(req.body.members).then(function () { + res.json({ message: "Group created", group: group }); + }); + }); + }) + .catch(function (err) { + next(err); }); - }); - }).catch(function(err) { - next(err); - }); -}); + } +); //return detail from just one group (open to all users) //redundant with /groups. I should probabaly depcreate this and implement query capability for /groups -router.get('/group/:id', jwt({secret: config.auth.public_key}), function(req, res) { - //if(!~req.user.scopes.sca.indexOf("admin")) return res.send(401); - db.Group.findOne({ - where: {id: req.params.id}, - include: [ - {model: db.User, as: 'Admins', attributes: ['id', 'email', 'fullname']}, - {model: db.User, as: 'Members', attributes: ['id', 'email', 'fullname']}, - ] - }).then(function(group) { - res.json(group); - }); -}); +router.get( + "/group/:id", + jwt({ secret: config.auth.public_key }), + function (req, res) { + //if(!~req.user.scopes.sca.indexOf("admin")) return res.send(401); + db.Group.findOne({ + where: { id: req.params.id }, + include: [ + { + model: db.User, + as: "Admins", + attributes: ["id", "email", "fullname"], + }, + { + model: db.User, + as: "Members", + attributes: ["id", "email", "fullname"], + }, + ], + }).then(function (group) { + res.json(group); + }); + } +); /** * @apiGroup Profile @@ -344,15 +454,19 @@ router.get('/group/:id', jwt({secret: config.auth.public_key}), function(req, re * * @apiSuccess {Object} updated user object */ -router.put('/profile', jwt({secret: config.auth.public_key}), function(req, res, next) { - db.User.findOne({where: {id: req.user.sub}}).then(function(user) { - user.fullname = req.body.fullname; - user.save().then(function() { - //res.json({status: "ok", message: "Account profile updated successfully."}); - res.json(user); +router.put( + "/profile", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + db.User.findOne({ where: { id: req.user.sub } }).then(function (user) { + user.fullname = req.body.fullname; + user.save().then(function () { + //res.json({status: "ok", message: "Account profile updated successfully."}); + res.json(user); + }); }); - }); -}); + } +); /** * @apiGroup Profile @@ -365,26 +479,29 @@ router.put('/profile', jwt({secret: config.auth.public_key}), function(req, res, * @apiParam {Number} limit Optional Maximum number of records to return - defaults to 100 * @apiParam {Number} offset Optional Record offset for pagination * - * @apiHeader {String} authorization + * @apiHeader {String} authorization * A valid JWT token "Bearer: xxxxx" */ -router.get('/profile', jwt({secret: config.auth.public_key}), function(req, res, next) { - var where = {}; - if(req.query.where) where = JSON.parse(req.query.where); - var order = [['fullname', 'DESC']]; - if(req.query.order) order = JSON.parse(req.query.order); +router.get( + "/profile", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + var where = {}; + if (req.query.where) where = JSON.parse(req.query.where); + var order = [["fullname", "DESC"]]; + if (req.query.order) order = JSON.parse(req.query.order); - db.User.findAndCountAll({ - where: where, - order: order, - limit: req.query.limit||100, - offset: req.query.offset||0, - attributes: [ 'id', 'fullname', 'email', 'active' ] - }).then(function(profiles) { - res.json({profiles: profiles.rows, count: profiles.count}); - }); + db.User.findAndCountAll({ + where: where, + order: order, + limit: req.query.limit || 100, + offset: req.query.offset || 0, + attributes: ["id", "fullname", "email", "active"], + }).then(function (profiles) { + res.json({ profiles: profiles.rows, count: profiles.count }); + }); - /* + /* db.User.find(find) .select(req.query.select || 'fullname') .select([ 'id', 'fullname', 'email', 'active']) @@ -403,16 +520,17 @@ router.get('/profile', jwt({secret: config.auth.public_key}), function(req, res, }); }); */ -}); + } +); //DEPRECATED BY GET:/profile //making this public for now (onere profile page) //router.get('/profile/:id', jwt({secret: config.auth.public_key}), function(req, res, next) { -router.get('/profile/:id', function(req, res, next) { +router.get("/profile/:id", function (req, res, next) { db.User.findOne({ - where: {id: req.params.id}, - attributes: [ 'id', 'fullname', 'email', 'active'] - }).then(function(user) { + where: { id: req.params.id }, + attributes: ["id", "fullname", "email", "active"], + }).then(function (user) { res.json(user); }); }); @@ -420,12 +538,16 @@ router.get('/profile/:id', function(req, res, next) { //DEPRECATED BY GET:/profile //return all profiles (open to all users) //(used by sca-wf-onere/project and others) -router.get('/profiles', jwt({secret: config.auth.public_key}), function(req, res) { - db.User.findAll({ - attributes: [ 'id', 'fullname', 'email', 'active'] - }).then(function(profiles) { - res.json(profiles); - }); -}); +router.get( + "/profiles", + jwt({ secret: config.auth.public_key }), + function (req, res) { + db.User.findAll({ + attributes: ["id", "fullname", "email", "active"], + }).then(function (profiles) { + res.json(profiles); + }); + } +); module.exports = router; diff --git a/api/controllers/signup.js b/api/controllers/signup.js index 6e28d36..5969a11 100644 --- a/api/controllers/signup.js +++ b/api/controllers/signup.js @@ -1,17 +1,16 @@ - //contrib -const express = require('express'); +const express = require("express"); const router = express.Router(); -const winston = require('winston'); -const clone = require('clone'); -const async = require('async'); -const jwt = require('express-jwt'); +const winston = require("winston"); +const clone = require("clone"); +const async = require("async"); +const jwt = require("express-jwt"); //mine -const config = require('../config'); +const config = require("../config"); const logger = new winston.Logger(config.logger.winston); -const db = require('../models'); -const common = require('../common'); +const db = require("../models"); +const common = require("../common"); function registerUser(body, done) { var u = clone(config.auth.default); @@ -20,35 +19,35 @@ function registerUser(body, done) { u.email = body.email; var user = db.User.build(u); //console.dir(user); - logger.info("registering new user: "+u.username); - user.setPassword(body.password, function(err) { - if(err) return done(err); + logger.info("registering new user: " + u.username); + user.setPassword(body.password, function (err) { + if (err) return done(err); logger.debug("set password done"); - user.save().then(function() { + user.save().then(function () { //add to default groups - user.addMemberGroups(u.gids, function() { - done(null, user); + user.addMemberGroups(u.gids, function () { + done(null, user); }); }); }); } function updateUser(req, done) { - db.User.findOne({where: {id: req.user.sub} }).then(function(user) { - if(!user) return done("can't find user"); + db.User.findOne({ where: { id: req.user.sub } }).then(function (user) { + if (!user) return done("can't find user"); //set things if it's not set yet - if(!user.username) user.username = req.body.username; - if(!user.fullname) user.fullname = req.body.fullname; - if(!user.email) user.email = req.body.email; - if(!user.password_hash) { - user.setPassword(req.body.password, function(err) { - if(err) return done(err); - user.save().then(()=>{ + if (!user.username) user.username = req.body.username; + if (!user.fullname) user.fullname = req.body.fullname; + if (!user.email) user.email = req.body.email; + if (!user.password_hash) { + user.setPassword(req.body.password, function (err) { + if (err) return done(err); + user.save().then(() => { done(null, user); }); }); } else { - user.save().then(()=>{ + user.save().then(() => { done(null, user); }); } @@ -67,61 +66,91 @@ function updateUser(req, done) { * */ -router.post('/', jwt({secret: config.auth.public_key, credentialsRequired: false}), function(req, res, next) { - var username = req.body.username; - var email = req.body.email; +router.post( + "/", + jwt({ secret: config.auth.public_key, credentialsRequired: false }), + function (req, res, next) { + var username = req.body.username; + var email = req.body.email; - function post_process(err, user) { - if(err) return next(err); - common.createClaim(user, function(err, claim) { - if(err) return next(err); - var jwt = common.signJwt(claim); - if(config.local.email_confirmation && !user.email_confirmed) { - common.send_email_confirmation(req.headers.referer||config.local.url, user, function(err) { - if(err) { - if(!req.user) { - //if we fail to send email, we should unregister the user we just created - user.destroy({force: true}).then(function() { - logger.error("removed newly registered record - email failure"); - res.status(500).json({message: "Failed to send confirmation email. Please make sure your email address is valid."}); - }); - } else { - res.status(500).json({message: "Failed to send confirmation email. Please make sure your email address is valid"}); + function post_process(err, user) { + if (err) return next(err); + common.createClaim(user, function (err, claim) { + if (err) return next(err); + var jwt = common.signJwt(claim); + if (config.local.email_confirmation && !user.email_confirmed) { + common.send_email_confirmation( + req.headers.referer || config.local.url, + user, + function (err) { + if (err) { + if (!req.user) { + //if we fail to send email, we should unregister the user we just created + user.destroy({ force: true }).then( + function () { + logger.error( + "removed newly registered record - email failure" + ); + res.status(500).json({ + message: + "Failed to send confirmation email. Please make sure your email address is valid.", + }); + } + ); + } else { + res.status(500).json({ + message: + "Failed to send confirmation email. Please make sure your email address is valid", + }); + } + } else { + res.json({ + path: "/confirm_email/" + user.id, + message: + "Confirmation Email has been sent. Please check your email inbox.", + jwt: jwt, + }); + } } + ); + } else { + //no need for email confrmation.. + res.json({ jwt: jwt, sub: user.id }); + } + }); + } + + //TODO - validate password strength? (req.body.password) + //check for username already taken + db.User.findOne({ where: { username: username } }).then(function ( + user + ) { + if (user) { + //TODO - maybe I should go ahead and forward user to login form? + return next( + "The username you chose is already registered. If it is yours, please try signing in, or register with a different username." + ); + } else { + //check for email already taken + db.User.findOne({ where: { email: email } }).then(function ( + user + ) { + if (user) { + //TODO - maybe I should go ahead and forward user to login form? + return next( + "The email address you chose is already registered. If it is yours, please try signing in, or register with a different email address." + ); } else { - res.json({path:'/confirm_email/'+user.id, message: "Confirmation Email has been sent. Please check your email inbox.", jwt: jwt}); + if (req.user) { + updateUser(req, post_process); + } else { + registerUser(req.body, post_process); + } } }); - } else { - //no need for email confrmation.. - res.json({jwt: jwt, sub: user.id}); } }); } - - //TODO - validate password strength? (req.body.password) - //check for username already taken - db.User.findOne({where: {username: username} }).then(function(user) { - if(user) { - //TODO - maybe I should go ahead and forward user to login form? - return next('The username you chose is already registered. If it is yours, please try signing in, or register with a different username.'); - } else { - //check for email already taken - db.User.findOne({where: {email: email} }).then(function(user) { - if(user) { - //TODO - maybe I should go ahead and forward user to login form? - return next('The email address you chose is already registered. If it is yours, please try signing in, or register with a different email address.'); - } else { - if(req.user) { - updateUser(req, post_process); - } else { - registerUser(req.body, post_process); - } - } - }); - } - }); -}) +); module.exports = router; - diff --git a/api/controllers/x509.js b/api/controllers/x509.js index 3c50ead..ae8549c 100644 --- a/api/controllers/x509.js +++ b/api/controllers/x509.js @@ -1,29 +1,30 @@ - //contrib -var express = require('express'); +var express = require("express"); var router = express.Router(); -var request = require('request'); -var winston = require('winston'); -var jwt = require('express-jwt'); +var request = require("request"); +var winston = require("winston"); +var jwt = require("express-jwt"); //mine -const config = require('../config'); +const config = require("../config"); var logger = new winston.Logger(config.logger.winston); -var common = require('../common'); -var db = require('../models'); +var common = require("../common"); +var db = require("../models"); function finduserByDN(dn, done) { - db.User.findOne({where: {x509dns: {$like: "%\""+dn+"\"%"}}}).then(function(user) { - done(null, user); - }); + db.User.findOne({ where: { x509dns: { $like: '%"' + dn + '"%' } } }).then( + function (user) { + done(null, user); + } + ); } function issue_jwt(user, dn, cb) { - user.updateTime('x509_login:'+dn); - user.save().then(function() { - var claim = common.createClaim(user, function(err, claim) { - if(err) return cb(err); + user.updateTime("x509_login:" + dn); + user.save().then(function () { + var claim = common.createClaim(user, function (err, claim) { + if (err) return cb(err); var jwt = common.signJwt(claim); cb(null, jwt); }); @@ -43,89 +44,150 @@ router.use(allowCrossDomain); //for OPTIONS method // this endpoint needs to be exposed via webserver that's requiring x509 DN // unlike /auth, this page will redirect back to #!/success/ -router.get('/signin', /*jwt({secret: config.auth.public_key, credentialsRequired: false}),*/ function(req, res, next) { - //logger.debug(req.user); - //res.header("Access-Control-Allow-Headers", "X-Requested-With"); - //return res.json({status: "ok", dn: req.headers[config.x509.dn_header], /*headers: req.headers*/}); - var dn = req.headers[config.x509.dn_header]; - if(!dn) { - console.dir(req.headers); - return next("Couldn't find x509 DN (maybe configuration issue?)"); - } - finduserByDN(dn, function(err, user) { - if(err) return next(err); - if(!user) return next("Your DN("+dn+") is not yet registered. Please Signup/Signin with your username/password first, then associate your x509 certificate under your account settings."); - var err = user.check(); - if(err) return next(err); - - //all good. issue token - logger.debug("x509 authentication successful with "+dn); - issue_jwt(user, dn, function(err, jwt) { - if(err) return next(err); - //res.json({jwt:jwt}); - res.redirect(req.headers.referer+"#!/success/"+jwt); - }); - }); -}); +router.get( + "/signin", + /*jwt({secret: config.auth.public_key, credentialsRequired: false}),*/ function ( + req, + res, + next + ) { + //logger.debug(req.user); + //res.header("Access-Control-Allow-Headers", "X-Requested-With"); + //return res.json({status: "ok", dn: req.headers[config.x509.dn_header], /*headers: req.headers*/}); + var dn = req.headers[config.x509.dn_header]; + if (!dn) { + console.dir(req.headers); + return next("Couldn't find x509 DN (maybe configuration issue?)"); + } + finduserByDN(dn, function (err, user) { + if (err) return next(err); + if (!user) + return next( + "Your DN(" + + dn + + ") is not yet registered. Please Signup/Signin with your username/password first, then associate your x509 certificate under your account settings." + ); + var err = user.check(); + if (err) return next(err); -// this endpoint needs to be exposed via webserver that's requiring x509 DN -router.get('/associate/:jwt', jwt({secret: config.auth.public_key, getToken: function(req) { return req.params.jwt; }}), -function(req, res, next) { -//router.get('/connect', jwt({secret: config.auth.public_key}), function(req, res, next) { - var dn = req.headers[config.x509.dn_header]; - if(!dn) { - console.dir(req.headers); - return next("Couldn't find x509 DN (maybe configuration issue?)"); - } - finduserByDN(dn, function(err, user) { - if(err) return next(err); - if(!user) { - //associate(req.user, dn, res); - logger.info("associating user with x509 DN "+dn); - db.User.findOne({where: {id: req.user.sub}}).then(function(user) { - if(!user) return next("couldn't find user record with jwt.sub:"+req.user.sub); - var dns = user.get('x509dns'); - if(!dns) dns = []; - if(!~dns.indexOf(dn)) dns.push(dn); - user.set('x509dns', dns); - user.save().then(function() { - var messages = [{type: "success", message: "Successfully associated your DN to your account."}]; - res.cookie('messages', JSON.stringify(messages)/*, {path: '/'}*/); - res.redirect(req.headers.referer+"#!/settings/account"); - }); + //all good. issue token + logger.debug("x509 authentication successful with " + dn); + issue_jwt(user, dn, function (err, jwt) { + if (err) return next(err); + //res.json({jwt:jwt}); + res.redirect(req.headers.referer + "#!/success/" + jwt); }); - } else { - var messages; - if(user.id == req.user.sub) { - messages = [{type: "info", message: "The certificate you have provided("+dn+") is already connected to your account."}]; + }); + } +); - } else { - messages = [{type: "error", message: "The certificate you have provided("+dn+") is already connected to another account."}]; - //TODO - does user wish to merge 2 accounts into 1? - } - res.cookie('messages', JSON.stringify(messages)/*, {path: '/'}*/); +// this endpoint needs to be exposed via webserver that's requiring x509 DN +router.get( + "/associate/:jwt", + jwt({ + secret: config.auth.public_key, + getToken: function (req) { + return req.params.jwt; + }, + }), + function (req, res, next) { + //router.get('/connect', jwt({secret: config.auth.public_key}), function(req, res, next) { + var dn = req.headers[config.x509.dn_header]; + if (!dn) { console.dir(req.headers); - res.redirect(req.headers.referer+"#!/settings/account"); + return next("Couldn't find x509 DN (maybe configuration issue?)"); } - }); -}); + finduserByDN(dn, function (err, user) { + if (err) return next(err); + if (!user) { + //associate(req.user, dn, res); + logger.info("associating user with x509 DN " + dn); + db.User.findOne({ where: { id: req.user.sub } }).then(function ( + user + ) { + if (!user) + return next( + "couldn't find user record with jwt.sub:" + + req.user.sub + ); + var dns = user.get("x509dns"); + if (!dns) dns = []; + if (!~dns.indexOf(dn)) dns.push(dn); + user.set("x509dns", dns); + user.save().then(function () { + var messages = [ + { + type: "success", + message: + "Successfully associated your DN to your account.", + }, + ]; + res.cookie( + "messages", + JSON.stringify(messages) /*, {path: '/'}*/ + ); + res.redirect( + req.headers.referer + "#!/settings/account" + ); + }); + }); + } else { + var messages; + if (user.id == req.user.sub) { + messages = [ + { + type: "info", + message: + "The certificate you have provided(" + + dn + + ") is already connected to your account.", + }, + ]; + } else { + messages = [ + { + type: "error", + message: + "The certificate you have provided(" + + dn + + ") is already connected to another account.", + }, + ]; + //TODO - does user wish to merge 2 accounts into 1? + } + res.cookie( + "messages", + JSON.stringify(messages) /*, {path: '/'}*/ + ); + console.dir(req.headers); + res.redirect(req.headers.referer + "#!/settings/account"); + } + }); + } +); -router.put('/disconnect', jwt({secret: config.auth.public_key}), function(req, res, next) { - var dn = req.body.dn; - logger.debug("disconnecting "+dn); - db.User.findOne({ - where: {id: req.user.sub} - }).then(function(user) { - if(!user) res.status(401).end(); - var dns = user.get('x509dns'); - var pos = dns.indexOf(dn); - if(~pos) dns.splice(pos, 1); - user.set('x509dns', dns); - user.save().then(function() { - res.json({message: "Successfully disconnected X509 DN:"+dn, user: user}); - }); - }); -}); +router.put( + "/disconnect", + jwt({ secret: config.auth.public_key }), + function (req, res, next) { + var dn = req.body.dn; + logger.debug("disconnecting " + dn); + db.User.findOne({ + where: { id: req.user.sub }, + }).then(function (user) { + if (!user) res.status(401).end(); + var dns = user.get("x509dns"); + var pos = dns.indexOf(dn); + if (~pos) dns.splice(pos, 1); + user.set("x509dns", dns); + user.save().then(function () { + res.json({ + message: "Successfully disconnected X509 DN:" + dn, + user: user, + }); + }); + }); + } +); module.exports = router; - diff --git a/api/migration.js b/api/migration.js index 0131ed0..6a40b76 100644 --- a/api/migration.js +++ b/api/migration.js @@ -1,14 +1,13 @@ - //contrib -var winston = require('winston'); -var async = require('async'); -var Promise = require('promise'); -var Sequelize = require('sequelize'); +var winston = require("winston"); +var async = require("async"); +var Promise = require("promise"); +var Sequelize = require("sequelize"); //mine -const config = require('./config'); +const config = require("./config"); var logger = new winston.Logger(config.logger.winston); -var db = require('./models'); +var db = require("./models"); var migrations = [ /* @@ -35,103 +34,132 @@ var migrations = [ }); }, */ - function(qi, next) { + function (qi, next) { logger.info("adding x509dn field for user table"); - qi.addColumn('Users', 'x509dns', {type: Sequelize.TEXT, defaultValue: '[]'}).then(function() { + qi.addColumn("Users", "x509dns", { + type: Sequelize.TEXT, + defaultValue: "[]", + }).then(function () { next(); }); }, - function(qi, next) { + function (qi, next) { logger.info("adding email_confirmed"); - qi.addColumn('Users', 'email_confirmed', {type: Sequelize.BOOLEAN, defaultValue: false}).then(function() { + qi.addColumn("Users", "email_confirmed", { + type: Sequelize.BOOLEAN, + defaultValue: false, + }).then(function () { next(); }); }, - function(qi, next) { + function (qi, next) { logger.info("adding email_confirmation_token"); - qi.addColumn('Users', 'email_confirmation_token', {type: Sequelize.STRING, /*defaultValue: uuid.v4()*/}).then(function() { + qi.addColumn("Users", "email_confirmation_token", { + type: Sequelize.STRING /*defaultValue: uuid.v4()*/, + }).then(function () { next(); }); }, - function(qi, next) { + function (qi, next) { logger.info("adding fullname"); - qi.addColumn('Users', 'fullname', {type: Sequelize.STRING}).then(function() { - next(); - }); + qi.addColumn("Users", "fullname", { type: Sequelize.STRING }).then( + function () { + next(); + } + ); }, - function(qi, next) { + function (qi, next) { logger.info("adding ldap"); - qi.addColumn('Users', 'ldap', {type: Sequelize.STRING}).then(function() { - next(); - }); + qi.addColumn("Users", "ldap", { type: Sequelize.STRING }).then( + function () { + next(); + } + ); }, - function(qi, next) { + function (qi, next) { logger.info("adding github ID"); - qi.addColumn('Users', 'github', {type: Sequelize.STRING}).then(function() { - next(); - }); + qi.addColumn("Users", "github", { type: Sequelize.STRING }).then( + function () { + next(); + } + ); }, - function(qi, next) { + function (qi, next) { logger.info("adding facebook ID"); - qi.addColumn('Users', 'facebook', {type: Sequelize.STRING}).then(function() { - next(); - }); + qi.addColumn("Users", "facebook", { type: Sequelize.STRING }).then( + function () { + next(); + } + ); }, - function(qi, next) { + function (qi, next) { next(); }, - function(qi, next) { + function (qi, next) { logger.info("adding password_reset_cookie"); - qi.addColumn('Users', 'password_reset_cookie', {type: Sequelize.STRING}).then(function() { + qi.addColumn("Users", "password_reset_cookie", { + type: Sequelize.STRING, + }).then(function () { next(); }); }, - function(qi, next) { + function (qi, next) { next(); }, - function(qi, next) { + function (qi, next) { logger.info("adding oidc_subs field for user table"); - qi.addColumn('Users', 'oidc_subs', {type: Sequelize.TEXT, defaultValue: '[]'}).then(function() { + qi.addColumn("Users", "oidc_subs", { + type: Sequelize.TEXT, + defaultValue: "[]", + }).then(function () { next(); }); }, - function(qi, next) { + function (qi, next) { logger.info("adding orcid"); - qi.addColumn('Users', 'orcid', {type: Sequelize.STRING}).then(function() { - next(); - }); + qi.addColumn("Users", "orcid", { type: Sequelize.STRING }).then( + function () { + next(); + } + ); }, ]; -exports.run = function() { +exports.run = function () { logger.debug("running migration"); - return new Promise(function(resolve, reject) { - db.Migration.findOne({}).then(function(info) { + return new Promise(function (resolve, reject) { + db.Migration.findOne({}).then(function (info) { //logger.debug(JSON.stringify(info, null, 4)); - if(!info) { + if (!info) { //assume brand new - skip everything - return db.Migration.create({version: migrations.length}).then(resolve); + return db.Migration.create({ version: migrations.length }).then( + resolve + ); } else { //var count = migrations.length; var ms = migrations.splice(info.version); qi = db.sequelize.getQueryInterface(); - async.eachSeries(ms, function(m, next) { - m(qi, function(err) { - if(err) return next(err); - info.increment('version'); - next(); - }); - }, function(err) { - if(err) reject(err); - else resolve("migration complete"); - /* + async.eachSeries( + ms, + function (m, next) { + m(qi, function (err) { + if (err) return next(err); + info.increment("version"); + next(); + }); + }, + function (err) { + if (err) reject(err); + else resolve("migration complete"); + /* info.save().then(function() { if(err) reject(err); else resolve("migration complete"); }); */ - }); + } + ); } }); }); -} +}; diff --git a/api/models/group.js b/api/models/group.js index 294b4d5..90145b4 100644 --- a/api/models/group.js +++ b/api/models/group.js @@ -1,23 +1,25 @@ -'use strict'; +"use strict"; //contrib -var Sequelize = require('sequelize'); -var winston = require('winston'); +var Sequelize = require("sequelize"); +var winston = require("winston"); //mine -const config = require('../config'); +const config = require("../config"); var logger = new winston.Logger(config.logger.winston); -module.exports = function(sequelize, DataTypes) { - return sequelize.define('Group', { - name: Sequelize.STRING, - desc: Sequelize.TEXT, - active: { type: Sequelize.BOOLEAN, defaultValue: true } - }, { - classMethods: { +module.exports = function (sequelize, DataTypes) { + return sequelize.define( + "Group", + { + name: Sequelize.STRING, + desc: Sequelize.TEXT, + active: { type: Sequelize.BOOLEAN, defaultValue: true }, }, - instanceMethods: { + { + classMethods: {}, + instanceMethods: {}, } - }); -} + ); +}; diff --git a/api/models/index.js b/api/models/index.js index 116d86e..c28895a 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,56 +1,60 @@ -'use strict'; +"use strict"; //node -var fs = require('fs'); -var path = require('path'); +var fs = require("fs"); +var path = require("path"); //contrib -var Sequelize = require('sequelize'); +var Sequelize = require("sequelize"); -const config = require('../config'); +const config = require("../config"); -var basename = path.basename(module.filename); +var basename = path.basename(module.filename); -if(typeof config.db === 'string') { +if (typeof config.db === "string") { var sequelize = new Sequelize(config.db, { /* logging: function(str) { //ignore for now.. } */ - logging: false + logging: false, }); } else { //assume object - var sequelize = new Sequelize(config.db.database, config.db.username, config.db.password, config.db); + var sequelize = new Sequelize( + config.db.database, + config.db.username, + config.db.password, + config.db + ); } var db = {}; -fs - .readdirSync(__dirname) - .filter(function(file) { - return (file.indexOf('.') !== 0) && (file !== basename); - }) - .forEach(function(file) { - if (file.slice(-3) !== '.js') return; - var model = sequelize['import'](path.join(__dirname, file)); - db[model.name] = model; - }); +fs.readdirSync(__dirname) + .filter(function (file) { + return file.indexOf(".") !== 0 && file !== basename; + }) + .forEach(function (file) { + if (file.slice(-3) !== ".js") return; + var model = sequelize["import"](path.join(__dirname, file)); + db[model.name] = model; + }); //I am not sure what this is for, but it's from the sequelize doc -Object.keys(db).forEach(function(modelName) { - if (db[modelName].associate) { - db[modelName].associate(db); - } +Object.keys(db).forEach(function (modelName) { + if (db[modelName].associate) { + db[modelName].associate(db); + } }); db.sequelize = sequelize; db.Sequelize = Sequelize; //relationships -db.User.belongsToMany(db.Group, {through: 'GroupAdmins'}); -db.Group.belongsToMany(db.User, {as: 'Admins', through: 'GroupAdmins'}); -db.User.belongsToMany(db.Group, {through: 'GroupMembers'}); -db.Group.belongsToMany(db.User, {as: 'Members', through: 'GroupMembers'}); +db.User.belongsToMany(db.Group, { through: "GroupAdmins" }); +db.Group.belongsToMany(db.User, { as: "Admins", through: "GroupAdmins" }); +db.User.belongsToMany(db.Group, { through: "GroupMembers" }); +db.Group.belongsToMany(db.User, { as: "Members", through: "GroupMembers" }); module.exports = db; diff --git a/api/models/migration.js b/api/models/migration.js index cdf8cfb..97430e5 100644 --- a/api/models/migration.js +++ b/api/models/migration.js @@ -1,23 +1,22 @@ -'use strict'; +"use strict"; -//this table holds current migration version +//this table holds current migration version //TODO - maybe I should rename this to "Info" or "Config" or such? //contrib -var Sequelize = require('sequelize'); -var winston = require('winston'); +var Sequelize = require("sequelize"); +var winston = require("winston"); //mine -const config = require('../config'); +const config = require("../config"); var logger = new winston.Logger(config.logger.winston); //for field types //http://docs.sequelizejs.com/en/latest/api/datatypes/ -module.exports = function(sequelize, DataTypes) { - return sequelize.define('Migration', { - version: Sequelize.INTEGER +module.exports = function (sequelize, DataTypes) { + return sequelize.define("Migration", { + version: Sequelize.INTEGER, }); -} - +}; diff --git a/api/models/user.js b/api/models/user.js index d09581a..65f49c0 100644 --- a/api/models/user.js +++ b/api/models/user.js @@ -1,122 +1,144 @@ -'use strict'; +"use strict"; //contrib -var Sequelize = require('sequelize'); -var JsonField = require('sequelize-json'); -var bcrypt = require('bcryptjs'); -var winston = require('winston'); -var async = require('async'); +var Sequelize = require("sequelize"); +var JsonField = require("sequelize-json"); +var bcrypt = require("bcryptjs"); +var winston = require("winston"); +var async = require("async"); //mine -const config = require('../config'); +const config = require("../config"); var logger = new winston.Logger(config.logger.winston); -module.exports = function(sequelize, DataTypes) { - return sequelize.define('User', { +module.exports = function (sequelize, DataTypes) { + return sequelize.define( + "User", + { + /////////////////////////////////////////////////////////////////////////////////////////// + //always filled (really?) + username: { type: Sequelize.STRING, unique: "true" }, - /////////////////////////////////////////////////////////////////////////////////////////// - //always filled (really?) - username: {type: Sequelize.STRING, unique: 'true'}, + /////////////////////////////////////////////////////////////////////////////////////////// + //auth profile + fullname: Sequelize.STRING, + email: { type: Sequelize.STRING, unique: "true" }, + email_confirmed: { type: Sequelize.BOOLEAN, defaultValue: false }, + email_confirmation_token: Sequelize.STRING, - /////////////////////////////////////////////////////////////////////////////////////////// - //auth profile - fullname: Sequelize.STRING, - email: {type: Sequelize.STRING, unique: 'true'}, - email_confirmed: { type: Sequelize.BOOLEAN, defaultValue: false }, - email_confirmation_token: Sequelize.STRING, + /////////////////////////////////////////////////////////////////////////////////////////// + //only used by local auth + password_hash: Sequelize.STRING, + password_reset_token: Sequelize.STRING, //used to reset password (via email?) + password_reset_cookie: Sequelize.STRING, //cookie token allowed to do reset + //password_reset_exp: Sequelize.DATE, - /////////////////////////////////////////////////////////////////////////////////////////// - //only used by local auth - password_hash: Sequelize.STRING, - password_reset_token: Sequelize.STRING, //used to reset password (via email?) - password_reset_cookie: Sequelize.STRING, //cookie token allowed to do reset - //password_reset_exp: Sequelize.DATE, + /////////////////////////////////////////////////////////////////////////////////////////// + //for 3rd party login (TODO - should I store all this in JSON fields?) + iucas: Sequelize.STRING, + ldap: Sequelize.STRING, + googleid: Sequelize.STRING, + github: Sequelize.STRING, + facebook: Sequelize.STRING, + x509dns: JsonField(sequelize, "User", "x509dns"), + orcid: Sequelize.STRING, - /////////////////////////////////////////////////////////////////////////////////////////// - //for 3rd party login (TODO - should I store all this in JSON fields?) - iucas: Sequelize.STRING, - ldap: Sequelize.STRING, - googleid: Sequelize.STRING, - github: Sequelize.STRING, - facebook: Sequelize.STRING, - x509dns: JsonField(sequelize, 'User', 'x509dns'), - orcid: Sequelize.STRING, + //oauth2: Sequelize.STRING, + oidc_subs: JsonField(sequelize, "User", "oidc_subs"), //openid connect subs - //oauth2: Sequelize.STRING, - oidc_subs: JsonField(sequelize, 'User', 'oidc_subs'), //openid connect subs + /////////////////////////////////////////////////////////////////////////////////////////// + // + times: JsonField(sequelize, "User", "times"), + scopes: JsonField(sequelize, "User", "scopes"), - /////////////////////////////////////////////////////////////////////////////////////////// - // - times: JsonField(sequelize, 'User', 'times'), - scopes: JsonField(sequelize, 'User', 'scopes'), - - //prevent user from loggin in (usually temporarily) - active: { type: Sequelize.BOOLEAN, defaultValue: true } - }, { - classMethods: { + //prevent user from loggin in (usually temporarily) + active: { type: Sequelize.BOOLEAN, defaultValue: true }, }, - instanceMethods: { - addMemberGroups: function(gids, cb) { - var rec = this; - async.forEach(gids, function(gid, next) { - sequelize.models.Group.findById(gid).then(function(group) { - if(!group) { - //need to register group first - var group = sequelize.models.Group.build({ - name: "tbd", - desc: "", - active: true, - }); - group.save().then(function() { - logger.info("adding new user to group "+gid); - group.addMember(rec.id).then(next); + { + classMethods: {}, + instanceMethods: { + addMemberGroups: function (gids, cb) { + var rec = this; + async.forEach( + gids, + function (gid, next) { + sequelize.models.Group.findById(gid).then(function ( + group + ) { + if (!group) { + //need to register group first + var group = sequelize.models.Group.build({ + name: "tbd", + desc: "", + active: true, + }); + group.save().then(function () { + logger.info( + "adding new user to group " + gid + ); + group.addMember(rec.id).then(next); + }); + } else { + logger.info( + "adding new user to group " + gid + ); + group.addMember(rec.id).then(next); + } }); - } else { - logger.info("adding new user to group "+gid); - group.addMember(rec.id).then(next); - } - }); - }, cb); - }, + }, + cb + ); + }, - setPassword: function (password, cb) { - var rec = this; - /* cost of computation https://www.npmjs.com/package/bcrypt - * rounds=10: ~10 hashes/sec - * rounds=13: ~1 sec/hash - * rounds=25: ~1 hour/hash - * rounds=31: 2-3 days/hash - */ - //logger.debug("generating sald"); - bcrypt.genSalt(10, function(err, salt) { - //logger.debug("encrypting pass"); - bcrypt.hash(password, salt, function(err, hash) { - //logger.debug("done "+err); - if(err) return cb(err); - //console.log("hash: "+hash); - rec.password_hash = hash; - cb(null); + setPassword: function (password, cb) { + var rec = this; + /* cost of computation https://www.npmjs.com/package/bcrypt + * rounds=10: ~10 hashes/sec + * rounds=13: ~1 sec/hash + * rounds=25: ~1 hour/hash + * rounds=31: 2-3 days/hash + */ + //logger.debug("generating sald"); + bcrypt.genSalt(10, function (err, salt) { + //logger.debug("encrypting pass"); + bcrypt.hash(password, salt, function (err, hash) { + //logger.debug("done "+err); + if (err) return cb(err); + //console.log("hash: "+hash); + rec.password_hash = hash; + cb(null); + }); }); - }); + }, + isPassword: function (password) { + if (!this.password_hash) return false; //no password, no go + return bcrypt.compareSync(password, this.password_hash); + }, + updateTime: function (key) { + var times = this.get("times"); + if (!times) times = {}; + times[key] = new Date(); + this.set("times", times); //not 100% if this is needed or not + }, + check: function () { + if (!this.active) + return { + message: "Account is disabled.", + code: "inactive", + }; + if ( + config.local && + config.local.email_confirmation && + this.email_confirmed !== true + ) { + return { + message: "Email is not confirmed yet", + path: "/confirm_email/" + this.id, + }; + } + return null; + }, }, - isPassword: function(password) { - if(!this.password_hash) return false; //no password, no go - return bcrypt.compareSync(password, this.password_hash); - }, - updateTime: function(key) { - var times = this.get('times'); - if(!times) times = {}; - times[key] = new Date(); - this.set('times', times); //not 100% if this is needed or not - }, - check: function() { - if(!this.active) return {message: "Account is disabled.", code: "inactive"}; - if(config.local && config.local.email_confirmation && this.email_confirmed !== true) { - return {message: "Email is not confirmed yet", path: "/confirm_email/"+this.id}; - } - return null; - } } - }); -} - + ); +}; diff --git a/api/server.js b/api/server.js index 8c17cdf..700abf0 100644 --- a/api/server.js +++ b/api/server.js @@ -1,32 +1,32 @@ #!/usr/bin/node -const config = require('./config'); +const config = require("./config"); -const express = require('express'); -const cookieParser = require('cookie-parser'); //google auth uses this -const bodyParser = require('body-parser'); -const Sequelize = require('sequelize'); -const passport = require('passport'); -const winston = require('winston'); -const expressWinston = require('express-winston'); -const cors = require('cors'); +const express = require("express"); +const cookieParser = require("cookie-parser"); //google auth uses this +const bodyParser = require("body-parser"); +const Sequelize = require("sequelize"); +const passport = require("passport"); +const winston = require("winston"); +const expressWinston = require("express-winston"); +const cors = require("cors"); const logger = new winston.Logger(config.logger.winston); -const db = require('./models'); -const migration = require('./migration'); +const db = require("./models"); +const migration = require("./migration"); //prevent startup if config is old -if(config.auth.default_scopes) { +if (config.auth.default_scopes) { throw new Error("default_scopes is replaced by default object in config."); } //init express var app = express(); app.use(bodyParser.json()); //parse application/json -app.use(bodyParser.urlencoded({extended: false})); //parse application/x-www-form-urlencoded //TODO - do we need this? -app.use(expressWinston.logger(config.logger.winston)); +app.use(bodyParser.urlencoded({ extended: false })); //parse application/x-www-form-urlencoded //TODO - do we need this? +app.use(expressWinston.logger(config.logger.winston)); app.use(cookieParser()); -app.use(passport.initialize());//needed for express-based application +app.use(passport.initialize()); //needed for express-based application app.use(cors()); //app.options('*', cors()); //enable pre-flight across the board @@ -40,44 +40,43 @@ app.all('*', (req, res, next)=>{ next(); }); */ -app.use('/', require('./controllers')); +app.use("/", require("./controllers")); //error handling app.use(expressWinston.errorLogger(config.logger.winston)); -app.use(function(err, req, res, next) { - if(typeof err == "string") err = {message: err}; +app.use(function (err, req, res, next) { + if (typeof err == "string") err = { message: err }; //log this error logger.info(err); - if(err.name) switch(err.name) { - case "UnauthorizedError": - logger.info(req.headers); //dump headers for debugging purpose.. - break; - } - if(err.stack) err.stack = "hidden"; //don't sent call stack to UI - for security reason + if (err.name) + switch (err.name) { + case "UnauthorizedError": + logger.info(req.headers); //dump headers for debugging purpose.. + break; + } + if (err.stack) err.stack = "hidden"; //don't sent call stack to UI - for security reason res.status(err.status || 500); res.json(err); }); -process.on('uncaughtException', function (err) { +process.on("uncaughtException", function (err) { //TODO report this to somewhere! - logger.error((new Date).toUTCString() + ' uncaughtException:', err.message) - logger.error(err.stack) -}) + logger.error(new Date().toUTCString() + " uncaughtException:", err.message); + logger.error(err.stack); +}); exports.app = app; -exports.start = function(cb) { +exports.start = function (cb) { db.sequelize - .sync(/*{force: true}*/) - .then(migration.run) - .then(function() { - var port = process.env.PORT || config.express.port || '8080'; - var host = process.env.HOST || config.express.host || 'localhost'; - app.listen(port, host, function(err) { - if(err) return cb(err); - console.log("Express server listening on %s:%d", host,port); - cb(null); + .sync(/*{force: true}*/) + .then(migration.run) + .then(function () { + var port = process.env.PORT || config.express.port || "8080"; + var host = process.env.HOST || config.express.host || "localhost"; + app.listen(port, host, function (err) { + if (err) return cb(err); + console.log("Express server listening on %s:%d", host, port); + cb(null); + }); }); - }); -} - - +}; diff --git a/bin/auth.js b/bin/auth.js index 13759b5..83c2c44 100755 --- a/bin/auth.js +++ b/bin/auth.js @@ -1,105 +1,126 @@ #!/usr/bin/env node -'use strict'; +"use strict"; /////////////////////////////////////////////////////////////////////////////////////////////////// // -// Update specified user's scope +// Update specified user's scope // -var config = require('../api/config'); +var config = require("../api/config"); //contrib -var argv = require('optimist').argv; -var winston = require('winston'); -var jwt = require('jsonwebtoken'); -var fs = require('fs'); -var _ = require('underscore'); -const readlineSync = require('readline-sync'); +var argv = require("optimist").argv; +var winston = require("winston"); +var jwt = require("jsonwebtoken"); +var fs = require("fs"); +var _ = require("underscore"); +const readlineSync = require("readline-sync"); var logger = new winston.Logger(config.logger.winston); -var db = require('../api/models'); - -const shortListCols = [ "id", "active", "username", "email", "fullname" ]; - -switch(argv._[0]) { - case "modscope": modscope(); break; - case "modrole": modrole(); break; - case "listuser": listuser(); break; - case "issue": issue(); break; - case "setpass": setpass(); break; - case "useradd": useradd(); break; - case "usermod": usermod(); break; - case "userdel": userdel(); break; +var db = require("../api/models"); + +const shortListCols = ["id", "active", "username", "email", "fullname"]; + +switch (argv._[0]) { + case "modscope": + modscope(); + break; + case "modrole": + modrole(); + break; + case "listuser": + listuser(); + break; + case "issue": + issue(); + break; + case "setpass": + setpass(); + break; + case "useradd": + useradd(); + break; + case "usermod": + usermod(); + break; + case "userdel": + userdel(); + break; default: - console.log(fs.readFileSync(__dirname+"/usage.txt", {encoding: "utf8"})); + console.log( + fs.readFileSync(__dirname + "/usage.txt", { encoding: "utf8" }) + ); break; } function listuser() { - if ( argv.id || argv.username || argv.email ) { - var condition = - {where: { - $or: [ - {id: argv.id}, - {username: argv.username}, - {email: argv.email}, - ]},raw: true} + if (argv.id || argv.username || argv.email) { + var condition = { + where: { + $or: [ + { id: argv.id }, + { username: argv.username }, + { email: argv.email }, + ], + }, + raw: true, + }; } else { - condition = {raw: true} + condition = { raw: true }; } - db.User.findAll(condition) - .then(function(users) { - if ( users.length < 1) { - console.error("No users found"); - } - var compact = argv.compact; - if ( ! argv.short ) { - if ( !compact ) { - console.log( JSON.stringify( users, null, " " ) ); - } else { - console.log( JSON.stringify( users ) ); - } + db.User.findAll(condition).then(function (users) { + if (users.length < 1) { + console.error("No users found"); + } + var compact = argv.compact; + if (!argv.short) { + if (!compact) { + console.log(JSON.stringify(users, null, " ")); } else { - console.log( shortListCols.join("\t") ); - _.map(users, function(user) { - var row = []; - _.each( shortListCols, function(col) { - row.push(user[col]); - }); - console.log(row.join("\t")); - }); + console.log(JSON.stringify(users)); } - }); - } - - function issue() { - if(!argv.scopes || argv.sub === undefined) { - - logger.error("pwa_auth issue --scopes '{common: [\"user\"]}' --sub 'my_service' [--exp 1514764800] [--out token.jwt] [--key test.key]"); - process.exit(1); + } else { + console.log(shortListCols.join("\t")); + _.map(users, function (user) { + var row = []; + _.each(shortListCols, function (col) { + row.push(user[col]); + }); + console.log(row.join("\t")); + }); } + }); +} - var claim = { - "iss": config.auth.iss, - "iat": (Date.now())/1000, - "sub": argv.sub, - }; - if(argv.scopes) { - claim.scopes = JSON.parse(argv.scopes); - } - if(argv.profile) { - claim.profile = JSON.parse(argv.profile); - } +function issue() { + if (!argv.scopes || argv.sub === undefined) { + logger.error( + "pwa_auth issue --scopes '{common: [\"user\"]}' --sub 'my_service' [--exp 1514764800] [--out token.jwt] [--key test.key]" + ); + process.exit(1); + } - if(argv.exp) { - claim.exp = argv.exp; - } - if(argv.key) { - console.log("using specified private key"); - config.auth.private_key = fs.readFileSync(argv.key); - } - /* + var claim = { + iss: config.auth.iss, + iat: Date.now() / 1000, + sub: argv.sub, + }; + if (argv.scopes) { + claim.scopes = JSON.parse(argv.scopes); + } + if (argv.profile) { + claim.profile = JSON.parse(argv.profile); + } + + if (argv.exp) { + claim.exp = argv.exp; + } + if (argv.key) { + console.log("using specified private key"); + config.auth.private_key = fs.readFileSync(argv.key); + } + /* if(argv.pub) { console.log("using specified public key"); config.auth.public_key = fs.readFileSync(argv.pub); @@ -107,14 +128,14 @@ function listuser() { } */ - var token = jwt.sign(claim, config.auth.private_key, config.auth.sign_opt); - if(argv.out) { - fs.writeFileSync(argv.out, token); - } else { - console.log(token); - } + var token = jwt.sign(claim, config.auth.private_key, config.auth.sign_opt); + if (argv.out) { + fs.writeFileSync(argv.out, token); + } else { + console.log(token); + } - /* + /* //verify to check jwt.verify(token, config.auth.public_key, function(err, decoded) { if(err) throw err; @@ -122,348 +143,371 @@ function listuser() { console.log(JSON.stringify(decoded, null, 4)); }); */ - } - - function modrole() { - if(!argv.username && !argv.id) { - logger.error("please specify --username (or --id ) --set/add/del '[\"user\",\"admin\"]'"); - process.exit(1); - } +} - function add(base, sub, scope) { - if(sub.constructor == Array) { - sub.forEach(function(item) { - if ( ! ( scope in base ) ) { - base[scope] = [ item ]; - } else { - if(!~base[scope].indexOf(item)) { - base[scope].push(item); - //base[scope] = item; - } +function modrole() { + if (!argv.username && !argv.id) { + logger.error( + 'please specify --username (or --id ) --set/add/del \'["user","admin"]\'' + ); + process.exit(1); + } + function add(base, sub, scope) { + if (sub.constructor == Array) { + sub.forEach(function (item) { + if (!(scope in base)) { + base[scope] = [item]; + } else { + if (!~base[scope].indexOf(item)) { + base[scope].push(item); + //base[scope] = item; } - }); - } else { - for(var k in sub) { - if(base[k] === undefined) base[k] = sub[k]; - else add(base[k], sub[k]); } + }); + } else { + for (var k in sub) { + if (base[k] === undefined) base[k] = sub[k]; + else add(base[k], sub[k]); } - return base; } + return base; + } - function del(base, sub, scope) { - if(typeof sub == 'object' && sub.constructor == Array) { - sub.forEach(function(item) { - var pos = base[scope].indexOf(item); - if(~pos) base[scope].splice(pos, 1); - }); - } else if(typeof sub == 'object') { - for(var k in sub) { - if(base[k] !== undefined) del(base[k], sub[k]); - } - } - if ( base[scope].length == 0 ) { - delete base[scope]; - + function del(base, sub, scope) { + if (typeof sub == "object" && sub.constructor == Array) { + sub.forEach(function (item) { + var pos = base[scope].indexOf(item); + if (~pos) base[scope].splice(pos, 1); + }); + } else if (typeof sub == "object") { + for (var k in sub) { + if (base[k] !== undefined) del(base[k], sub[k]); } - return base; } - - db.User.findOne({where: { - $or: [ - {username: argv.username}, - {id: argv.id}, - ]} - }).then(function(user) { - if(!user) return logger.error("can't find user:"+argv.username); - if ( !( argv.set || argv.add || argv.del ) ) { - logger.error("No action specified to modify role "); - process.exit(1); - } - var scope = argv.scope; - if(argv.set) { - user.scopes[ scope ] = JSON.parse(argv.set); - } - if ( typeof scope == "undefined" || scope === true ) { - logger.error("no scope specified"); - process.exit(1); - - } - - if(argv.add) { - user.scopes = add(_.clone(user.scopes), JSON.parse(argv.add), scope); - } - if(argv.del) { - if ( ! ( scope in user.scopes ) ) { - logger.info("Scope not exist; cannot delete roles within it"); - process.exit(1); - } - user.scopes = del(_.clone(user.scopes), JSON.parse(argv.del), scope); - } - user.save().then(function() { - logger.info("successfully updated user role. user must re-login for it to take effect)"); - logger.info("Updated scopes: ", JSON.stringify(user.scopes)); - }).catch(function(err) { - logger.error(err); - }); - }) + if (base[scope].length == 0) { + delete base[scope]; + } + return base; } - function modscope() { - if(!argv.username && !argv.id) { - logger.error("please specify --username (or --id ) --set/add/del '{{common: [\"user\", \"admin\"]}}'"); + db.User.findOne({ + where: { + $or: [{ username: argv.username }, { id: argv.id }], + }, + }).then(function (user) { + if (!user) return logger.error("can't find user:" + argv.username); + if (!(argv.set || argv.add || argv.del)) { + logger.error("No action specified to modify role "); process.exit(1); } - if ( !( argv.set || argv.add || argv.del ) ) { - logger.error("No action specified to modify scope"); - process.exit(1); + var scope = argv.scope; + if (argv.set) { + user.scopes[scope] = JSON.parse(argv.set); } - if ( argv.scope ) { - logger.error("Invalid parameter --scope. Did you mean to run 'modrole'?"); + if (typeof scope == "undefined" || scope === true) { + logger.error("no scope specified"); process.exit(1); + } + if (argv.add) { + user.scopes = add( + _.clone(user.scopes), + JSON.parse(argv.add), + scope + ); } + if (argv.del) { + if (!(scope in user.scopes)) { + logger.info("Scope not exist; cannot delete roles within it"); + process.exit(1); + } + user.scopes = del( + _.clone(user.scopes), + JSON.parse(argv.del), + scope + ); + } + user.save() + .then(function () { + logger.info( + "successfully updated user role. user must re-login for it to take effect)" + ); + logger.info("Updated scopes: ", JSON.stringify(user.scopes)); + }) + .catch(function (err) { + logger.error(err); + }); + }); +} - function add(base, sub) { - if(sub.constructor == Array) { - sub.forEach(function(item) { - if(!~base.indexOf(item)) base.push(item); - }); - } else { - for(var k in sub) { - if(base[k] === undefined) base[k] = sub[k]; - else add(base[k], sub[k]); - } +function modscope() { + if (!argv.username && !argv.id) { + logger.error( + 'please specify --username (or --id ) --set/add/del \'{{common: ["user", "admin"]}}\'' + ); + process.exit(1); + } + if (!(argv.set || argv.add || argv.del)) { + logger.error("No action specified to modify scope"); + process.exit(1); + } + if (argv.scope) { + logger.error( + "Invalid parameter --scope. Did you mean to run 'modrole'?" + ); + process.exit(1); + } + + function add(base, sub) { + if (sub.constructor == Array) { + sub.forEach(function (item) { + if (!~base.indexOf(item)) base.push(item); + }); + } else { + for (var k in sub) { + if (base[k] === undefined) base[k] = sub[k]; + else add(base[k], sub[k]); } - return base; } + return base; + } - function del(base, sub) { - if(typeof sub == 'object' && sub.constructor == Array) { - sub.forEach(function(item) { - delete base[item]; - /* + function del(base, sub) { + if (typeof sub == "object" && sub.constructor == Array) { + sub.forEach(function (item) { + delete base[item]; + /* var pos = base.indexOf(item); if(~pos) base.splice(pos, 1); */ - }); - } else if(typeof sub == 'object') { - Object.keys(sub).forEach(function( k, index, self) { - delete base[k]; - delete sub[k]; - }); - } - return base; + }); + } else if (typeof sub == "object") { + Object.keys(sub).forEach(function (k, index, self) { + delete base[k]; + delete sub[k]; + }); } + return base; + } - db.User.findOne({where: { - $or: [ - {username: argv.username}, - {id: argv.id}, - ]} - }).then(function(user) { - if(!user) return logger.error("can't find user:"+argv.username); - if(argv.set) { - user.scopes = JSON.parse(argv.set); - } - if(argv.add) { - user.scopes = add(_.clone(user.scopes), JSON.parse(argv.add)); - } - if(argv.del) { - user.scopes = del(_.clone(user.scopes), JSON.parse(argv.del)); - } - user.save().then(function() { - logger.info("successfully updated user scope. user must re-login for it to take effect)"); + db.User.findOne({ + where: { + $or: [{ username: argv.username }, { id: argv.id }], + }, + }).then(function (user) { + if (!user) return logger.error("can't find user:" + argv.username); + if (argv.set) { + user.scopes = JSON.parse(argv.set); + } + if (argv.add) { + user.scopes = add(_.clone(user.scopes), JSON.parse(argv.add)); + } + if (argv.del) { + user.scopes = del(_.clone(user.scopes), JSON.parse(argv.del)); + } + user.save() + .then(function () { + logger.info( + "successfully updated user scope. user must re-login for it to take effect)" + ); process.exit(); - }).catch(function(err) { + }) + .catch(function (err) { logger.error(err); process.exit(1); }); - }) + }); +} + +function setpass() { + if (!argv.username && !argv.id) { + logger.error("please specify --username or --id "); + process.exit(1); } - function setpass() { - if(!argv.username && !argv.id) { - logger.error("please specify --username or --id "); - process.exit(1); + db.User.findOne({ + where: { + $or: [{ username: argv.username }, { id: argv.id }], + }, + }).then(function (user) { + if (!user) return logger.error("can't find user:" + argv.username); + var newPassword = argv.password; + if (typeof newPassword == "undefined") { + // 'true' indicates we should prompt user for password + newPassword = true; } - - db.User.findOne({where: { - $or: [ - {username: argv.username}, - {id: argv.id}, - ]} - }).then(function(user) { - if(!user) return logger.error("can't find user:"+argv.username); - var newPassword = argv.password; - if ( typeof newPassword == "undefined" ) { - // 'true' indicates we should prompt user for password - newPassword = true; - } - if ( newPassword === true ) { - var confirmPassword; - while (confirmPassword != newPassword ) { - newPassword = readlineSync.question('Please enter new password: ', { - hideEchoBack: true // The typed text on screen is hidden by `*` (default). - }); - confirmPassword = readlineSync.question('Please confirm new password: ', { - hideEchoBack: true // The typed text on screen is hidden by `*` (default). - }); - if ( newPassword != confirmPassword ) { - console.log("passwords don't match; try again"); + if (newPassword === true) { + var confirmPassword; + while (confirmPassword != newPassword) { + newPassword = readlineSync.question( + "Please enter new password: ", + { + hideEchoBack: true, // The typed text on screen is hidden by `*` (default). } + ); + confirmPassword = readlineSync.question( + "Please confirm new password: ", + { + hideEchoBack: true, // The typed text on screen is hidden by `*` (default). + } + ); + if (newPassword != confirmPassword) { + console.log("passwords don't match; try again"); } - if ( newPassword == confirmPassword && newPassword != "" ) { - console.log("updating password"); - } else { - return logger.error("error updating password for user "+user.username + "; password must not be empty."); - - } - } + if (newPassword == confirmPassword && newPassword != "") { + console.log("updating password"); + } else { + return logger.error( + "error updating password for user " + + user.username + + "; password must not be empty." + ); + } + } - user.setPassword(newPassword, function(err) { - if(err) throw err; - user.save().then(function() { + user.setPassword(newPassword, function (err) { + if (err) throw err; + user.save() + .then(function () { logger.log("successfully updated password"); - }).catch(function(err) { + }) + .catch(function (err) { logger.error(err); }); - }); - }) - } - - function usermod() { - var updateFields = {}; - if(!argv.id) { - logger.error("You must specify a user id to modify, like this: --id "); - process.exit(1); - } - - if(argv.username && argv.username !== true) { - logger.info("New username specified: " + argv.username); - updateFields.username = argv.username; - } - if(argv.email && argv.email !== true) { - logger.info("New email specified: " + argv.email); - updateFields.email = argv.email; - } - if(argv.fullname && argv.fullname !== true) { - logger.info("New fullname specified: " + argv.fullname); - updateFields.fullname = argv.fullname; - } - var uniqueFieldChecks = { - username: argv.username, - email: argv.email - }; - var userExists = false; - // make sure the user exists - db.User.findOne({where: - {id: argv.id} - }).then(function(user) { - if(!user) return logger.error("can't find user:"+argv.id); - if ( user.username == argv.username || argv.username == null ) delete uniqueFieldChecks.username; - if ( user.email == argv.email || argv.email == null ) delete uniqueFieldChecks.email; - db.User.findOne({where: { - $or: [ - uniqueFieldChecks - ]} - }).then(function(user) { - //console.log("prevent duplicate user"); - if ( user ) userExists = true; - - if ( userExists ) { - logger.error("Error: not updating user; username and email must be unique"); - process.exit(1); - } + }); + }); +} - // make sure the user exists - db.User.findOne({where: - {id: argv.id} - }).then(function(user) { - if(!user) return logger.error("can't find user:"+argv.id); - var newPassword = argv.password; - }); +function usermod() { + var updateFields = {}; + if (!argv.id) { + logger.error( + "You must specify a user id to modify, like this: --id " + ); + process.exit(1); + } - // update the values - db.User.update( - updateFields, - {where: {id: argv.id} - }).then(function(user) { - console.log("Updated user "); - }); + if (argv.username && argv.username !== true) { + logger.info("New username specified: " + argv.username); + updateFields.username = argv.username; + } + if (argv.email && argv.email !== true) { + logger.info("New email specified: " + argv.email); + updateFields.email = argv.email; + } + if (argv.fullname && argv.fullname !== true) { + logger.info("New fullname specified: " + argv.fullname); + updateFields.fullname = argv.fullname; + } + var uniqueFieldChecks = { + username: argv.username, + email: argv.email, + }; + var userExists = false; + // make sure the user exists + db.User.findOne({ where: { id: argv.id } }).then(function (user) { + if (!user) return logger.error("can't find user:" + argv.id); + if (user.username == argv.username || argv.username == null) + delete uniqueFieldChecks.username; + if (user.email == argv.email || argv.email == null) + delete uniqueFieldChecks.email; + db.User.findOne({ + where: { + $or: [uniqueFieldChecks], + }, + }).then(function (user) { + //console.log("prevent duplicate user"); + if (user) userExists = true; + + if (userExists) { + logger.error( + "Error: not updating user; username and email must be unique" + ); + process.exit(1); + } + // make sure the user exists + db.User.findOne({ where: { id: argv.id } }).then(function (user) { + if (!user) return logger.error("can't find user:" + argv.id); + var newPassword = argv.password; }); + + // update the values + db.User.update(updateFields, { where: { id: argv.id } }).then( + function (user) { + console.log("Updated user "); + } + ); }); + }); +} +function useradd() { + if (!argv.username) { + logger.error("please specify --username "); + process.exit(1); + } + if (!argv.fullname) { + logger.error("please specify --fullname "); + process.exit(1); + } + if (!argv.email) { + logger.error("please specify --email "); + process.exit(1); } - function useradd() { - if(!argv.username) { - logger.error("please specify --username "); - process.exit(1); - } - if(!argv.fullname) { - logger.error("please specify --fullname "); + // Check whether username/email already exist + // Theoretically sequelize should prevent dupes, but that constraint is + // not working + var userExists = false; + db.User.findOne({ + where: { + $or: [{ username: argv.username }, { email: argv.email }], + }, + }).then(function (user) { + if (user) userExists = true; + + if (userExists) { + logger.error( + "User already exists; username and email must be unique" + ); process.exit(1); } - if(!argv.email) { - logger.error("please specify --email "); - process.exit(1); - } - - // Check whether username/email already exist - // Theoretically sequelize should prevent dupes, but that constraint is - // not working - var userExists = false; - db.User.findOne({where: { - $or: [ - {username: argv.username}, - {email: argv.email}, - ]} - }).then(function(user) { - if ( user ) userExists = true; - - if ( userExists ) { - logger.error("User already exists; username and email must be unique"); - process.exit(1); - } - var user = db.User.build( - //extend from default - Object.assign({ + var user = db.User.build( + //extend from default + Object.assign( + { username: argv.username, fullname: argv.fullname, email: argv.email, email_confirmed: true, - }, config.auth.default) - ); - user.save().then(function(_user) { - if(!_user) return logger.error("couldn't register new user"); - logger.info("successfully created a user"); - if(argv.password) setpass(); - else logger.info("you might want to reset password / setscope"); - }); + }, + config.auth.default + ) + ); + user.save().then(function (_user) { + if (!_user) return logger.error("couldn't register new user"); + logger.info("successfully created a user"); + if (argv.password) setpass(); + else logger.info("you might want to reset password / setscope"); }); - } - - function userdel() { - if(!argv.username && !argv.id) { - logger.error("please specify --username or --id "); - process.exit(1); - } + }); +} - //TODO - does this cascade to group? - db.User.destroy({ - where: { $or: [ - {username: argv.username}, - {id: argv.id}, - ]} - }).then(function(count) { - if(count == 1) logger.info("successfully removed user"); - else logger.info("failed to remove user - maybe doesn't exist?") - }); +function userdel() { + if (!argv.username && !argv.id) { + logger.error("please specify --username or --id "); + process.exit(1); } - + //TODO - does this cascade to group? + db.User.destroy({ + where: { $or: [{ username: argv.username }, { id: argv.id }] }, + }).then(function (count) { + if (count == 1) logger.info("successfully removed user"); + else logger.info("failed to remove user - maybe doesn't exist?"); + }); +} diff --git a/config/default.json b/config/default.json index 389ad27..9ac389a 100644 --- a/config/default.json +++ b/config/default.json @@ -1,5 +1,5 @@ { - "name": "PWA Auth", - "configPath": "/etc/perfsonar/psconfig-web", - "port": 3000 + "name": "PWA Auth", + "configPath": "/etc/perfsonar/psconfig-web", + "port": 3000 } diff --git a/etc/shared/auth.ui.js b/etc/shared/auth.ui.js index 8cc62ac..e7a7c0c 100644 --- a/etc/shared/auth.ui.js +++ b/etc/shared/auth.ui.js @@ -1,31 +1,29 @@ -'use strict'; +"use strict"; -angular.module('app.config', []) -.constant('appconf', { +angular.module("app.config", []).constant("appconf", { + title: "PWA Authentication Service", - title: 'PWA Authentication Service', + admin_email: "user@domain.tld", + logo_400_url: "images/pscweb_logo.png", - admin_email: 'user@domain.tld', - logo_400_url: 'images/pscweb_logo.png', - - oidc_logo: 'images/cilogon.png', + oidc_logo: "images/cilogon.png", //URL for auth service API - api: '../api/auth', + api: "../api/auth", //URL for x509 validation API - x509api: 'https://'+location.hostname+':9443', + x509api: "https://" + location.hostname + ":9443", //default location to redirect after successful login - //default_redirect_url: '../profile', - default_redirect_url: '/pwa/', + //default_redirect_url: '../profile', + default_redirect_url: "/pwa/", - jwt_id: 'jwt', - iucas_url: 'https://cas.iu.edu/cas/login', + jwt_id: "jwt", + iucas_url: "https://cas.iu.edu/cas/login", jwt_whitelist: [location.hostname], - //show/hide various login options + //show/hide various login options show: { local: true, ldap: false, @@ -44,4 +42,3 @@ angular.module('app.config', []) signup: true, }, }); - diff --git a/example/get_groups.js b/example/get_groups.js index fad065d..a475b12 100755 --- a/example/get_groups.js +++ b/example/get_groups.js @@ -1,19 +1,27 @@ -'use strict'; +"use strict"; -var request = require('request'); -var fs = require('fs'); +var request = require("request"); +var fs = require("fs"); var sca = "https://soichi7.ppa.iu.edu/api"; -var jwt = fs.readFileSync('/home/hayashis/git/sca/config/sca.jwt', {encoding: 'ascii'}).trim(); +var jwt = fs + .readFileSync("/home/hayashis/git/sca/config/sca.jwt", { + encoding: "ascii", + }) + .trim(); //register new user //using SCA local username and password -request.get({ - url: sca+"/auth/user/groups/1", json: true, - headers: { 'Authorization': 'Bearer '+jwt }, -}, function(err, res, users) { - if(err) throw err; - console.log(res.statusCode+" "+res.statusMessage); - console.dir(users); -}); +request.get( + { + url: sca + "/auth/user/groups/1", + json: true, + headers: { Authorization: "Bearer " + jwt }, + }, + function (err, res, users) { + if (err) throw err; + console.log(res.statusCode + " " + res.statusMessage); + console.dir(users); + } +); diff --git a/example/login.js b/example/login.js index d1849e9..6f3aa53 100755 --- a/example/login.js +++ b/example/login.js @@ -1,18 +1,22 @@ -'use strict'; +"use strict"; -var request = require('request'); +var request = require("request"); //var sca = "https://test.sca.iu.edu/api"; //register new user //using SCA local username and password -request.post({ - url: "https://soichi7.ppa.iu.edu/api/auth/local/auth", json: true, - body: { - username: "user5", - password: "password123" +request.post( + { + url: "https://soichi7.ppa.iu.edu/api/auth/local/auth", + json: true, + body: { + username: "user5", + password: "password123", + }, + }, + function (err, res, body) { + if (err) throw err; + console.dir(body); } -}, function(err, res, body) { - if(err) throw err; - console.dir(body); -}); +); diff --git a/example/query_users.js b/example/query_users.js index 44b0121..0084d1e 100755 --- a/example/query_users.js +++ b/example/query_users.js @@ -1,25 +1,33 @@ -'use strict'; +"use strict"; -var request = require('request'); -var fs = require('fs'); +var request = require("request"); +var fs = require("fs"); var sca = "https://soichi7.ppa.iu.edu/api"; -var jwt = fs.readFileSync('/home/hayashis/git/sca/config/sca.jwt', {encoding: 'ascii'}).trim(); +var jwt = fs + .readFileSync("/home/hayashis/git/sca/config/sca.jwt", { + encoding: "ascii", + }) + .trim(); //register new user //using SCA local username and password -request.get({ - url: sca+"/auth/users", json: true, - headers: { 'Authorization': 'Bearer '+jwt }, - qs: { - gids: true, //load gid - where: JSON.stringify({ - id: 1, - }) +request.get( + { + url: sca + "/auth/users", + json: true, + headers: { Authorization: "Bearer " + jwt }, + qs: { + gids: true, //load gid + where: JSON.stringify({ + id: 1, + }), + }, }, -}, function(err, res, users) { - if(err) throw err; - console.log(res.statusCode+" "+res.statusMessage); - console.dir(users); -}); + function (err, res, users) { + if (err) throw err; + console.log(res.statusCode + " " + res.statusMessage); + console.dir(users); + } +); diff --git a/example/refresh.js b/example/refresh.js index be5f6ad..db32fd9 100755 --- a/example/refresh.js +++ b/example/refresh.js @@ -1,18 +1,24 @@ -'use strict'; +"use strict"; -var request = require('request'); -var fs = require('fs'); +var request = require("request"); +var fs = require("fs"); var sca = "https://soichi7.ppa.iu.edu/api"; -var jwt = fs.readFileSync('/home/hayashis/.sca/keys/cli.jwt', {encoding: 'ascii'}).trim(); +var jwt = fs + .readFileSync("/home/hayashis/.sca/keys/cli.jwt", { encoding: "ascii" }) + .trim(); //register new user //using SCA local username and password -request.post({ - url: sca+"/auth/refresh", json: true, - headers: { 'Authorization': 'Bearer '+jwt }, -}, function(err, res, body) { - if(err) throw err; - console.dir(body); -}); +request.post( + { + url: sca + "/auth/refresh", + json: true, + headers: { Authorization: "Bearer " + jwt }, + }, + function (err, res, body) { + if (err) throw err; + console.dir(body); + } +); diff --git a/example/register_key.js b/example/register_key.js index 2edfa71..8b86892 100755 --- a/example/register_key.js +++ b/example/register_key.js @@ -1,18 +1,22 @@ -'use strict'; +"use strict"; -var request = require('request'); +var request = require("request"); var sca = "https://test.sca.iu.edu/api"; //register new user //using SCA local username and password -request.post({ - url: sca+"/auth/local/auth", json: true, - body: { - username: "user5", - password: "password123" +request.post( + { + url: sca + "/auth/local/auth", + json: true, + body: { + username: "user5", + password: "password123", + }, + }, + function (err, res, body) { + if (err) throw err; + console.dir(body); } -}, function(err, res, body) { - if(err) throw err; - console.dir(body); -}); +); diff --git a/example/users.js b/example/users.js index ad2f464..05d1028 100755 --- a/example/users.js +++ b/example/users.js @@ -1,22 +1,28 @@ -'use strict'; +"use strict"; -var fs = require('fs'); -var request = require('request'); +var fs = require("fs"); +var request = require("request"); var sca = "https://soichi7.ppa.iu.edu/api"; -var jwt = fs.readFileSync('/home/hayashis/.sca/keys/cli.jwt', {encoding: 'ascii'}).trim(); +var jwt = fs + .readFileSync("/home/hayashis/.sca/keys/cli.jwt", { encoding: "ascii" }) + .trim(); //register new user //using SCA local username and password -request.get({ - url: sca+"/auth/users", json: true, - headers: { 'Authorization': 'Bearer '+jwt }, - body: { - //where: todo, +request.get( + { + url: sca + "/auth/users", + json: true, + headers: { Authorization: "Bearer " + jwt }, + body: { + //where: todo, + }, + }, + function (err, res, body) { + if (err) throw err; + console.log(res.statusCode + " " + res.statusMessage); + console.dir(body); } -}, function(err, res, body) { - if(err) throw err; - console.log(res.statusCode + " " + res.statusMessage); - console.dir(body); -}); +); diff --git a/test/common.js b/test/common.js index f263f33..d94deba 100644 --- a/test/common.js +++ b/test/common.js @@ -1,25 +1,24 @@ +const assert = require("assert"); +const chai = require("chai"); -const assert = require('assert'); -const chai = require('chai'); +const common = require("../api/common"); -const common = require('../api/common'); - -describe("common", function() { - it('intersect_scopes', function(done) { +describe("common", function () { + it("intersect_scopes", function (done) { var s1 = { - "a": [ "1", "2", "3" ], - "b": [ "1", "2", "3" ], - "c": [ "4" ], - "d": [ ], - } + a: ["1", "2", "3"], + b: ["1", "2", "3"], + c: ["4"], + d: [], + }; var s2 = { - "b": [ "2", "5" ], - "d": [ "4" ], - } - var ans = { - "b": [ "2" ], - "d": [ ], - } + b: ["2", "5"], + d: ["4"], + }; + var ans = { + b: ["2"], + d: [], + }; var inter = common.intersect_scopes(s1, s2); chai.expect(inter).to.deep.equal(ans); //reverse should be the same @@ -28,15 +27,14 @@ describe("common", function() { done(); }); - it('intersect_scopes(one side empty)', function(done) { + it("intersect_scopes(one side empty)", function (done) { var s1 = { - "a": [ "1", "2", "3" ], - "b": [ "1", "2", "3" ], - "c": [ "4" ], - "d": [ ], - } - var s2 = { - } + a: ["1", "2", "3"], + b: ["1", "2", "3"], + c: ["4"], + d: [], + }; + var s2 = {}; var inter = common.intersect_scopes(s1, s2); chai.expect(inter).to.deep.equal({}); //reverse should be the same @@ -44,13 +42,11 @@ describe("common", function() { chai.expect(inter).to.deep.equal({}); done(); }); - it('intersect_scopes(body empty)', function(done) { - var s1 = {} - var s2 = {} + it("intersect_scopes(body empty)", function (done) { + var s1 = {}; + var s2 = {}; var inter = common.intersect_scopes(s1, s2); chai.expect(inter).to.deep.equal({}); done(); }); }); - - diff --git a/test/test.js b/test/test.js index a451cac..b8af265 100644 --- a/test/test.js +++ b/test/test.js @@ -1,38 +1,37 @@ - -const request = require('supertest') -const assert = require('assert'); -const fs = require('fs'); +const request = require("supertest"); +const assert = require("assert"); +const fs = require("fs"); //mine -var config = require('../api/config'); +var config = require("../api/config"); config.local = {}; //force local to be avaialble... (TODO should I use a dedicated test config?) config.test = { - jwt: fs.readFileSync('./api/config/test.jwt','ascii').trim(), -} + jwt: fs.readFileSync("./api/config/test.jwt", "ascii").trim(), +}; //use temporary db for test.. config.db.storage = "/tmp/test.sqlite"; -var db = require('../api/models'); -var app = require('../api/server').app; +var db = require("../api/models"); +var app = require("../api/server").app; -before(function(done) { +before(function (done) { console.log("synching sequelize"); this.timeout(10000); - db.sequelize.sync({force: true}).then(function() { + db.sequelize.sync({ force: true }).then(function () { console.log("synchronized"); done(); }); }); -describe('GET /health', function() { - it('return 200', function(done) { +describe("GET /health", function () { + it("return 200", function (done) { request(app) - .get('/health') - .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect(200, done); + .get("/health") + .set("Accept", "application/json") + .expect("Content-Type", /json/) + .expect(200, done); }); }); @@ -47,97 +46,97 @@ describe('GET /config', function() { }); }); */ -describe('/local', function() { - describe("create user", function() { - it('should create user 1', function(done) { +describe("/local", function () { + describe("create user", function () { + it("should create user 1", function (done) { var user = db.User.build({ - username: 'test', - fullname: 'test user', - email: 'test@example.com', + username: "test", + fullname: "test user", + email: "test@example.com", scopes: config.auth.default_scopes, }); user.email_confirmation_token = "abc123"; //console.dir(user); - user.setPassword('testpass', function(err) { + user.setPassword("testpass", function (err) { //console.log("set password"); - if(err) throw err; - user.save().then(function(_user) { + if (err) throw err; + user.save().then(function (_user) { done(); }); }); }); - it('should create user 2', function(done) { + it("should create user 2", function (done) { var user = db.User.build({ - username: 'test2', - fullname: 'test user2', - email: 'test2@example.com', + username: "test2", + fullname: "test user2", + email: "test2@example.com", scopes: config.auth.default_scopes, }); user.email_confirmation_token = "abc123"; - user.setPassword('testpass', function(err) { - if(err) throw err; - user.save().then(function(_user) { + user.setPassword("testpass", function (err) { + if (err) throw err; + user.save().then(function (_user) { done(); }); }); }); }); - describe("/confirming_email", function() { - it('should confirm', function(done) { + describe("/confirming_email", function () { + it("should confirm", function (done) { request(app) - .post('/confirm_email') - .send({token: "abc123"}) - .expect(200, done) + .post("/confirm_email") + .send({ token: "abc123" }) + .expect(200, done); }); }); - describe('/local/auth', function() { - it('returns valid token', function(done) { + describe("/local/auth", function () { + it("returns valid token", function (done) { request(app) - .post('/local/auth') - .send({username: 'test', password: 'testpass'}) - .expect(200) - .end(function(err, res) { - if(err) return done(err); - console.dir(res.body); - done(); - }); + .post("/local/auth") + .send({ username: "test", password: "testpass" }) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + console.dir(res.body); + done(); + }); }); }); }); -describe('/root', function() { - describe("get/profile", function() { - it('get all', function(done) { +describe("/root", function () { + describe("get/profile", function () { + it("get all", function (done) { request(app) - .get('/profile') - .set('Accept', 'application/json') - .set('Authorization', 'Bearer '+config.test.jwt) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if(err) return done(err); - assert(res.body.profiles.length == 2); - assert(res.body.count == 2); - console.dir(res.body); - done(); - }); + .get("/profile") + .set("Accept", "application/json") + .set("Authorization", "Bearer " + config.test.jwt) + .expect("Content-Type", /json/) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + assert(res.body.profiles.length == 2); + assert(res.body.count == 2); + console.dir(res.body); + done(); + }); }); - it('get 1', function(done) { + it("get 1", function (done) { request(app) - .get('/profile?limit=1&offset=1') - .set('Accept', 'application/json') - .set('Authorization', 'Bearer '+config.test.jwt) - .expect('Content-Type', /json/) - .expect(200) - .end(function(err, res) { - if(err) return done(err); - assert(res.body.profiles.length == 1); - assert(res.body.count == 2); - console.dir(res.body); - done(); - }); + .get("/profile?limit=1&offset=1") + .set("Accept", "application/json") + .set("Authorization", "Bearer " + config.test.jwt) + .expect("Content-Type", /json/) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + assert(res.body.profiles.length == 1); + assert(res.body.count == 2); + console.dir(res.body); + done(); + }); }); }); }); @@ -251,5 +250,3 @@ after(function(done) { }); */ - - diff --git a/ui/common.js b/ui/common.js index 13a7ced..d646fad 100644 --- a/ui/common.js +++ b/ui/common.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /* function handle_auth_issues(res) { diff --git a/ui/css/style.css b/ui/css/style.css index e97db9f..77ba830 100644 --- a/ui/css/style.css +++ b/ui/css/style.css @@ -1,65 +1,65 @@ .center-block { - display: block; - margin-top: 50px; - margin-left: auto; - margin-right: auto; - padding: 30px; - max-width: 500px; - background-color: white; - box-shadow: 0px 0px 15px #666; - border-radius: 2px; + display: block; + margin-top: 50px; + margin-left: auto; + margin-right: auto; + padding: 30px; + max-width: 500px; + background-color: white; + box-shadow: 0px 0px 15px #666; + border-radius: 2px; } .logo { - margin: 0px 0px 30px 0px; - width: 100%; + margin: 0px 0px 30px 0px; + width: 100%; } .list-group-item-heading { - margin-bottom: 0px; + margin-bottom: 0px; } .header { - padding: 10px; - padding-left: 20px; - background-color: #eee; + padding: 10px; + padding-left: 20px; + background-color: #eee; } .header h2 { - margin: 5px; + margin: 5px; } .settings img.account { - height: 27px; - margin-top: 8px; + height: 27px; + margin-top: 8px; } .settings li.heading { - background-color: #eee; + background-color: #eee; } .settings li.heading-clickable:hover { - cursor: pointer; - background-color: #ccc; + cursor: pointer; + background-color: #ccc; } .edit_button { - position: absolute; - right: 0px; - top: 0px; - z-order: 1; + position: absolute; + right: 0px; + top: 0px; + z-order: 1; } ul.userlist { - padding: 0px; - list-style: none; - font-size: 12px; - margin-bottom: 0px; + padding: 0px; + list-style: none; + font-size: 12px; + margin-bottom: 0px; } ul.userlist li { - background-color: #f0f0f0; - padding: 4px 6px; - display: inline-block; - margin: 1px 3px; + background-color: #f0f0f0; + padding: 4px 6px; + display: inline-block; + margin: 1px 3px; } .ui-select-match-close { - color: #222; + color: #222; } .fixed-top { - position: fixed; - top: 0px; - left: 0px; - z-index: 999; - width: 100%; + position: fixed; + top: 0px; + left: 0px; + z-index: 999; + width: 100%; } diff --git a/ui/css/style.less b/ui/css/style.less index 5ffff4b..d06bce1 100644 --- a/ui/css/style.less +++ b/ui/css/style.less @@ -1,4 +1,3 @@ - .center-block { display: block; margin-top: 50px; @@ -12,7 +11,7 @@ } .logo { - margin:0px 0px 30px 0px; + margin: 0px 0px 30px 0px; width: 100%; } @@ -33,7 +32,7 @@ img.account { height: 27px; margin-top: 8px; - } + } li.heading { background-color: #eee; } @@ -73,4 +72,3 @@ ul.userlist { z-index: 999; width: 100%; } - diff --git a/ui/index.html b/ui/index.html index 6371eae..544d768 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1,47 +1,70 @@ - - - - - Loading ... + + + + + Loading ... - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + - - - - - + + + + + diff --git a/ui/iucascb.html b/ui/iucascb.html index 04ebcd9..dd20ec7 100644 --- a/ui/iucascb.html +++ b/ui/iucascb.html @@ -1,72 +1,101 @@ - - - - - - - + + + + + + + +
+

+
+ Verifying your CAS token ... + +
+

+
-
-

-
- Verifying your CAS token ... - -
-

-
+ + + + - - - - + + - - - - - + app.run(function ($http, appconf, $cookies, scaMessage, $location) { + console.log("iucascb::app.run ref:" + document.referrer); + var casticket = getParameterByName("casticket"); + $http + .get(appconf.api + "/iucas/verify?casticket=" + casticket) + .then( + function (res) { + localStorage.setItem(appconf.jwt_id, res.data.jwt); + var redirect = + sessionStorage.getItem("auth_redirect"); + sessionStorage.removeItem("auth_redirect"); + scaMessage.success("Successfully logged in"); + console.log("done.. redirecting " + redirect); + if (redirect) window.location = redirect; + else window.location = appconf.default_redirect_url; + }, + function (res) { + console.dir(res); + if (res.data && res.data.path) { + window.location = "./#!" + res.data.path; + if (res.data.message) + scaMessage.error(res.data.message); + } else { + if (res.data && res.data.message) + scaMessage.error(res.data.message); + else + scaMessage.error( + res.statusText || + "Oops.. unknown authentication error" + ); + window.location = "./"; + } + } + ); + }); + + - diff --git a/ui/js/app.js b/ui/js/app.js index 1e10d22..3bea897 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -1,215 +1,247 @@ -'use strict'; +"use strict"; -var app = angular.module('app', [ - 'app.config', - 'ngRoute', - 'ngAnimate', - 'ngCookies', - 'toaster', - 'angular-loading-bar', - 'angular-jwt', - 'ui.bootstrap', - 'sca-shared', - 'ui.gravatar', - 'ui.select' +var app = angular.module("app", [ + "app.config", + "ngRoute", + "ngAnimate", + "ngCookies", + "toaster", + "angular-loading-bar", + "angular-jwt", + "ui.bootstrap", + "sca-shared", + "ui.gravatar", + "ui.select", ]); -app.config(['$qProvider', function ($qProvider) { +app.config([ + "$qProvider", + function ($qProvider) { $qProvider.errorOnUnhandledRejections(false); -}]); + }, +]); //http://wijmo.com/easy-form-validation-in-angularjs/ //note - error message only shows when user submit the form -app.directive('match', function () { - return { - require: 'ngModel', - link: function (scope, elm, attrs, ctl) { - scope.$watch(attrs['match'], function (errorMsg) { - elm[0].setCustomValidity(errorMsg); - ctl.$setValidity('match', errorMsg ? false : true); - }); - } - }; +app.directive("match", function () { + return { + require: "ngModel", + link: function (scope, elm, attrs, ctl) { + scope.$watch(attrs["match"], function (errorMsg) { + elm[0].setCustomValidity(errorMsg); + ctl.$setValidity("match", errorMsg ? false : true); + }); + }, + }; }); -app.directive('validjson', function () { - return { - require: 'ngModel', - link: function (scope, elm, attrs, ctrl) { - ctrl.$parsers.unshift(function(value) { - var valid = true; - try { - JSON.parse(value); - elm[0].setCustomValidity(''); - } catch (e) { - elm[0].setCustomValidity("Couldn't parse JSON"); - valid = false; - } - ctrl.$setValidity('validjson', valid); - return valid ? value : undefined; - }); - } - }; +app.directive("validjson", function () { + return { + require: "ngModel", + link: function (scope, elm, attrs, ctrl) { + ctrl.$parsers.unshift(function (value) { + var valid = true; + try { + JSON.parse(value); + elm[0].setCustomValidity(""); + } catch (e) { + elm[0].setCustomValidity("Couldn't parse JSON"); + valid = false; + } + ctrl.$setValidity("validjson", valid); + return valid ? value : undefined; + }); + }, + }; }); -app.factory('profiles', function(appconf, $http, jwtHelper, toaster) { - return $http.get(appconf.api+'/profiles') - .then(function(res) { - return res.data; - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); +app.factory("profiles", function (appconf, $http, jwtHelper, toaster) { + return $http.get(appconf.api + "/profiles").then( + function (res) { + return res.data; + }, + function (res) { + if (res.data && res.data.message) toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); }); //show loading bar at the top -app.config(['cfpLoadingBarProvider', function(cfpLoadingBarProvider) { - cfpLoadingBarProvider.includeSpinner = false; -}]); +app.config([ + "cfpLoadingBarProvider", + function (cfpLoadingBarProvider) { + cfpLoadingBarProvider.includeSpinner = false; + }, +]); //set allowed jwt provider -app.config(function(appconf, $httpProvider, jwtOptionsProvider) { +app.config(function (appconf, $httpProvider, jwtOptionsProvider) { jwtOptionsProvider.config({ - whiteListedDomains: appconf.jwt_whitelist, + whiteListedDomains: appconf.jwt_whitelist, }); }); //configure route -app.config(['$routeProvider', 'appconf', function($routeProvider, appconf) { - $routeProvider. - when('/signin', { - templateUrl: 't/signin.html', - controller: 'SigninController' - }). - when('/success/:jwt', { - template: '', - controller: 'SuccessController' - }) - .when('/signout', { - template: '', - controller: 'SignoutController' - }) - .when('/forgotpass/:token?', { - templateUrl: 't/forgotpass.html', - controller: 'ForgotpassController' - }) - .when('/signup/:jwt?', { - templateUrl: 't/signup.html', - controller: 'SignupController' - }) - .when('/settings/account', { - templateUrl: 't/account.html', - controller: 'AccountController', - requiresLogin: true - }) - .when('/admin/users', { - templateUrl: 't/adminusers.html', - controller: 'AdminUsersController', - requiresLogin: true - }) - .when('/admin/user/:id', { - templateUrl: 't/adminuser.html', - controller: 'AdminUserController', - requiresLogin: true - }) - .when('/groups', { - templateUrl: 't/groups.html', - controller: 'GroupsController', - requiresLogin: true - }) - .when('/group/:id', { - templateUrl: 't/group.html', - controller: 'GroupController', - requiresLogin: true - }) - .when('/inactive', { - templateUrl: 't/inactive.html', - //controller: 'AdminUserController', - }) - .when('/confirm_email/:sub?/:token?', { - templateUrl: 't/confirm_email.html', - controller: 'ConfirmEmailController', - }) - .otherwise({ - redirectTo: '/signin' - }); -}]).run(['$rootScope', '$location', 'toaster', 'jwtHelper', 'appconf', function($rootScope, $location, toaster, jwtHelper, appconf) { - $rootScope.$on("$routeChangeStart", function(event, next, current) { - //console.log("route changed from "+current+" to :"+next); - //redirect to /login if user hasn't authenticated yet - if(next.requiresLogin) { - var jwt = localStorage.getItem(appconf.jwt_id); - if(jwt == null || jwtHelper.isTokenExpired(jwt)) { - toaster.warning("Please sign in first"); - sessionStorage.setItem('auth_redirect', '#!'+next.originalPath); - $location.path("/signin"); - event.preventDefault(); +app.config([ + "$routeProvider", + "appconf", + function ($routeProvider, appconf) { + $routeProvider + .when("/signin", { + templateUrl: "t/signin.html", + controller: "SigninController", + }) + .when("/success/:jwt", { + template: "", + controller: "SuccessController", + }) + .when("/signout", { + template: "", + controller: "SignoutController", + }) + .when("/forgotpass/:token?", { + templateUrl: "t/forgotpass.html", + controller: "ForgotpassController", + }) + .when("/signup/:jwt?", { + templateUrl: "t/signup.html", + controller: "SignupController", + }) + .when("/settings/account", { + templateUrl: "t/account.html", + controller: "AccountController", + requiresLogin: true, + }) + .when("/admin/users", { + templateUrl: "t/adminusers.html", + controller: "AdminUsersController", + requiresLogin: true, + }) + .when("/admin/user/:id", { + templateUrl: "t/adminuser.html", + controller: "AdminUserController", + requiresLogin: true, + }) + .when("/groups", { + templateUrl: "t/groups.html", + controller: "GroupsController", + requiresLogin: true, + }) + .when("/group/:id", { + templateUrl: "t/group.html", + controller: "GroupController", + requiresLogin: true, + }) + .when("/inactive", { + templateUrl: "t/inactive.html", + //controller: 'AdminUserController', + }) + .when("/confirm_email/:sub?/:token?", { + templateUrl: "t/confirm_email.html", + controller: "ConfirmEmailController", + }) + .otherwise({ + redirectTo: "/signin", + }); + }, +]).run([ + "$rootScope", + "$location", + "toaster", + "jwtHelper", + "appconf", + function ($rootScope, $location, toaster, jwtHelper, appconf) { + $rootScope.$on("$routeChangeStart", function (event, next, current) { + //console.log("route changed from "+current+" to :"+next); + //redirect to /login if user hasn't authenticated yet + if (next.requiresLogin) { + var jwt = localStorage.getItem(appconf.jwt_id); + if (jwt == null || jwtHelper.isTokenExpired(jwt)) { + toaster.warning("Please sign in first"); + sessionStorage.setItem( + "auth_redirect", + "#!" + next.originalPath + ); + $location.path("/signin"); + event.preventDefault(); + } } - } - }); -}]); + }); + }, +]); //configure httpProvider to send jwt unless skipAuthorization is set in config (not tested yet..) -app.config(function(appconf, $httpProvider, jwtInterceptorProvider) { - jwtInterceptorProvider.tokenGetter = function(jwtHelper, $http) { +app.config(function (appconf, $httpProvider, jwtInterceptorProvider) { + jwtInterceptorProvider.tokenGetter = function (jwtHelper, $http) { //don't send jwt for template requests //if (config.url.substr(config.url.length - 5) == '.html') return null; return localStorage.getItem(appconf.jwt_id); - } - $httpProvider.interceptors.push('jwtInterceptor'); + }; + $httpProvider.interceptors.push("jwtInterceptor"); }); -app.factory('menu', function(appconf, $http, jwtHelper, $sce, toaster, $rootScope) { - var menu = { - header: { - //label: appconf.title, - }, - //top: scaMenu, - user: null, //to-be-loaded - _profile: null, //to-be-loaded - }; - if(appconf.icon_url) menu.header.icon = $sce.trustAsHtml(""); - if(appconf.home_url) menu.header.url = appconf.home_url - - var jwt = localStorage.getItem(appconf.jwt_id); - if(jwt) apply_jwt(jwt); - function apply_jwt(jwt) { - console.log("applying jwt"); - console.log(jwt); - try { - var expdate = jwtHelper.getTokenExpirationDate(jwt); - } catch (e) { - toaster.error(e); - localStorage.removeItem(appconf.jwt_id); - } - var ttl = expdate - Date.now(); - if(ttl < 0) { - toaster.error("Your login session has expired. Please re-sign in"); - localStorage.removeItem(appconf.jwt_id); - } else { - menu.user = jwtHelper.decodeToken(jwt); - if(ttl < 3600*1000) { - //jwt expring in less than an hour! refresh! - console.log("jwt expiring in an hour.. refreshing first"); - $http({ - url: appconf.auth_api+'/refresh', - //skipAuthorization: true, //prevent infinite recursion - //headers: {'Authorization': 'Bearer '+jwt}, - method: 'POST' - }).then(function(response) { - var jwt = response.data.jwt; - localStorage.setItem(appconf.jwt_id, jwt); - menu.user = jwtHelper.decodeToken(jwt); - }); +app.factory( + "menu", + function (appconf, $http, jwtHelper, $sce, toaster, $rootScope) { + var menu = { + header: { + //label: appconf.title, + }, + //top: scaMenu, + user: null, //to-be-loaded + _profile: null, //to-be-loaded + }; + if (appconf.icon_url) + menu.header.icon = $sce.trustAsHtml( + '' + ); + if (appconf.home_url) menu.header.url = appconf.home_url; + + var jwt = localStorage.getItem(appconf.jwt_id); + if (jwt) apply_jwt(jwt); + function apply_jwt(jwt) { + console.log("applying jwt"); + console.log(jwt); + try { + var expdate = jwtHelper.getTokenExpirationDate(jwt); + } catch (e) { + toaster.error(e); + localStorage.removeItem(appconf.jwt_id); + } + var ttl = expdate - Date.now(); + if (ttl < 0) { + toaster.error( + "Your login session has expired. Please re-sign in" + ); + localStorage.removeItem(appconf.jwt_id); + } else { + menu.user = jwtHelper.decodeToken(jwt); + if (ttl < 3600 * 1000) { + //jwt expring in less than an hour! refresh! + console.log("jwt expiring in an hour.. refreshing first"); + $http({ + url: appconf.auth_api + "/refresh", + //skipAuthorization: true, //prevent infinite recursion + //headers: {'Authorization': 'Bearer '+jwt}, + method: "POST", + }).then(function (response) { + var jwt = response.data.jwt; + localStorage.setItem(appconf.jwt_id, jwt); + menu.user = jwtHelper.decodeToken(jwt); + }); + } } } - } - //when jwt is updated, I need to re-apply it to update the menu - $rootScope.$on('jwt_update', function(event, jwt) {apply_jwt(jwt); }); + //when jwt is updated, I need to re-apply it to update the menu + $rootScope.$on("jwt_update", function (event, jwt) { + apply_jwt(jwt); + }); - return menu; -}); + return menu; + } +); //http://plnkr.co/edit/juqoNOt1z1Gb349XabQ2?p=preview /** @@ -218,41 +250,43 @@ app.factory('menu', function(appconf, $http, jwtHelper, $sce, toaster, $rootScop * performs a AND between 'name: $select.search' and 'age: $select.search'. * We want to perform a OR. */ -app.filter('propsFilter', function() { - return function(items, props) { - var out = []; +app.filter("propsFilter", function () { + return function (items, props) { + var out = []; - if (angular.isArray(items)) { - items.forEach(function(item) { - var itemMatches = false; + if (angular.isArray(items)) { + items.forEach(function (item) { + var itemMatches = false; - var keys = Object.keys(props); - for (var i = 0; i < keys.length; i++) { - var prop = keys[i]; - var text = props[prop].toLowerCase(); - if (item[prop] && item[prop].toString().toLowerCase().indexOf(text) !== -1) { - itemMatches = true; - break; - } - } + var keys = Object.keys(props); + for (var i = 0; i < keys.length; i++) { + var prop = keys[i]; + var text = props[prop].toLowerCase(); + if ( + item[prop] && + item[prop].toString().toLowerCase().indexOf(text) !== -1 + ) { + itemMatches = true; + break; + } + } - if (itemMatches) { - out.push(item); + if (itemMatches) { + out.push(item); + } + }); + } else { + // Let the output be the input untouched + out = items; } - }); - } else { - // Let the output be the input untouched - out = items; - } - return out; - }; + return out; + }; }); -app.directive('userlist', function() { +app.directive("userlist", function () { return { - scope: { users: '=', }, - templateUrl: 't/userlist.html', - } + scope: { users: "=" }, + templateUrl: "t/userlist.html", + }; }); - diff --git a/ui/js/controllers.js b/ui/js/controllers.js index 143aced..bdf6aee 100644 --- a/ui/js/controllers.js +++ b/ui/js/controllers.js @@ -1,264 +1,366 @@ -'use strict'; - -app.controller('HeaderController', -function($scope, appconf, $route, toaster, $http, menu, scaSettingsMenu) { - $scope.title = appconf.title; - $scope.menu = menu; - $scope.appconf = appconf; - $scope.settings_menu = scaSettingsMenu; -}); - -app.controller('SigninController', -function($scope, $route, toaster, $http, $routeParams, $location, scaMessage, $sce, $rootScope) { - $scope.$parent.active_menu = 'signin'; - scaMessage.show(toaster); - - if($routeParams.msg) { - toaster.error($routeParams.msg); - } - - function handle_success(res) { - localStorage.setItem($scope.appconf.jwt_id, res.data.jwt); - $rootScope.$broadcast("jwt_update", res.data.jwt) - handle_redirect($scope); - } - - function handle_error(res) { - if(res.data && res.data.path) { - //console.log("path requested "+res.data.path); - $location.path(res.data.path); - if(res.data.message) scaMessage.error(res.data.message); - } else { - console.dir(res); - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText || "Oops.. unknown authentication error"); +"use strict"; + +app.controller( + "HeaderController", + function ($scope, appconf, $route, toaster, $http, menu, scaSettingsMenu) { + $scope.title = appconf.title; + $scope.menu = menu; + $scope.appconf = appconf; + $scope.settings_menu = scaSettingsMenu; + } +); + +app.controller( + "SigninController", + function ( + $scope, + $route, + toaster, + $http, + $routeParams, + $location, + scaMessage, + $sce, + $rootScope + ) { + $scope.$parent.active_menu = "signin"; + scaMessage.show(toaster); + + if ($routeParams.msg) { + toaster.error($routeParams.msg); } - } - $scope.userpass = {}; - $scope.submit = function() { - var url = ""; - //ldap auth takes precedence - if($scope.appconf.show.local) url = $scope.appconf.api+"/local/auth"; - if($scope.appconf.show.ldap) url = $scope.appconf.api+"/ldap/auth"; - $http.post(url, $scope.userpass).then(handle_success, handle_error); - } - - $scope.begin_iucas = function() { - //I can't pass # for callback somehow (I don't know how to make it work, or iucas removes it) - //so let's let another html page handle the callback, do the token validation through iucas and generate the jwt - //and either redirect to profile page (default) or force user to setup user/pass if it's brand new user - var casurl = window.location.origin+window.location.pathname+'iucascb.html'; - window.location = $scope.appconf.iucas_url+'?cassvc=IU&casurl='+casurl; - } - - $scope.begin = function(type) { - //console.log("$scope.appconf", $scope.appconf); - var api_prefix = $scope.appconf.api || "/pwa/"; - window.location = api_prefix +"/"+ type+"/signin"; - } - - $scope.begin_x509 = function() { - //TODO - can't use CORS for x509 auth.. - //$http.get($scope.appconf.x509api+"/x509/auth") - //.then(handle_success, handle_error); - window.location = $scope.appconf.x509api+"/x509/signin"; - } - $scope.begin_oidc = function(idp) { - window.location = "/api/auth/oidc/signin?idp="+encodeURIComponent(idp); - } + function handle_success(res) { + localStorage.setItem($scope.appconf.jwt_id, res.data.jwt); + $rootScope.$broadcast("jwt_update", res.data.jwt); + handle_redirect($scope); + } - function getQueryVariable(variable) { - var query = window.location.search.substring(1); - var vars = query.split('&'); - for (var i = 0; i < vars.length; i++) { - var pair = vars[i].split('='); - if (decodeURIComponent(pair[0]) == variable) { - return decodeURIComponent(pair[1]); + function handle_error(res) { + if (res.data && res.data.path) { + //console.log("path requested "+res.data.path); + $location.path(res.data.path); + if (res.data.message) scaMessage.error(res.data.message); + } else { + console.dir(res); + if (res.data && res.data.message) + toaster.error(res.data.message); + else + toaster.error( + res.statusText || "Oops.. unknown authentication error" + ); } } - console.log('Query variable %s not found', variable); - } - $scope.refreshIDPs = function(query) { - if(!query) { - $scope.oidc_idps = []; - return; + $scope.userpass = {}; + $scope.submit = function () { + var url = ""; + //ldap auth takes precedence + if ($scope.appconf.show.local) + url = $scope.appconf.api + "/local/auth"; + if ($scope.appconf.show.ldap) + url = $scope.appconf.api + "/ldap/auth"; + $http.post(url, $scope.userpass).then(handle_success, handle_error); + }; + + $scope.begin_iucas = function () { + //I can't pass # for callback somehow (I don't know how to make it work, or iucas removes it) + //so let's let another html page handle the callback, do the token validation through iucas and generate the jwt + //and either redirect to profile page (default) or force user to setup user/pass if it's brand new user + var casurl = + window.location.origin + + window.location.pathname + + "iucascb.html"; + window.location = + $scope.appconf.iucas_url + "?cassvc=IU&casurl=" + casurl; + }; + + $scope.begin = function (type) { + //console.log("$scope.appconf", $scope.appconf); + var api_prefix = $scope.appconf.api || "/pwa/"; + window.location = api_prefix + "/" + type + "/signin"; + }; + + $scope.begin_x509 = function () { + //TODO - can't use CORS for x509 auth.. + //$http.get($scope.appconf.x509api+"/x509/auth") + //.then(handle_success, handle_error); + window.location = $scope.appconf.x509api + "/x509/signin"; + }; + $scope.begin_oidc = function (idp) { + window.location = + "/api/auth/oidc/signin?idp=" + encodeURIComponent(idp); + }; + + function getQueryVariable(variable) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split("="); + if (decodeURIComponent(pair[0]) == variable) { + return decodeURIComponent(pair[1]); + } + } + console.log("Query variable %s not found", variable); } - console.log("refreshing idps", query); - $http.get($scope.appconf.api+"/oidc/idp", {params: {q: query}}) - .then(function(res) { - $scope.oidc_idps = res.data; - }) - .catch(function(res) { - toaster.error("failed to load IDP list"); - console.error(res); - }); + + $scope.refreshIDPs = function (query) { + if (!query) { + $scope.oidc_idps = []; + return; + } + console.log("refreshing idps", query); + $http + .get($scope.appconf.api + "/oidc/idp", { params: { q: query } }) + .then(function (res) { + $scope.oidc_idps = res.data; + }) + .catch(function (res) { + toaster.error("failed to load IDP list"); + console.error(res); + }); + }; } -}); +); function handle_redirect($scope) { console.log("scope.appconf", $scope.appconf); - var redirect = sessionStorage.getItem('auth_redirect'); - sessionStorage.removeItem('auth_redirect'); + var redirect = sessionStorage.getItem("auth_redirect"); + sessionStorage.removeItem("auth_redirect"); var default_url = $scope.appconf.default_redirect_url; - if (typeof default_url == "undefined" ) { + if (typeof default_url == "undefined") { console.log("setting default redirect url to '/'"); - default_url = "/" + default_url = "/"; } var referrer; - if ( document.referrer != "" ) { + if (document.referrer != "") { referrer = document.referrer; } - window.location = redirect||document.referrer||default_url; + window.location = redirect || document.referrer || default_url; //window.location = redirect||"/pwa/"; } //used by oauth2 callbacks (github, etc..) to set the jwt and redirect -app.controller('SuccessController', -function($scope, $route, $http, $routeParams, $location, scaMessage, $sce, $rootScope) { - console.log("successcontroller called"); - scaMessage.success("Welcome back!"); - localStorage.setItem($scope.appconf.jwt_id, $routeParams.jwt); - $rootScope.$broadcast("jwt_update", $routeParams.jwt); - handle_redirect($scope); -}); - -app.controller('SignoutController', -function($scope, $route, $http, $routeParams, menu, $location, scaMessage) { - scaMessage.success("Good Bye!"); - localStorage.removeItem($scope.appconf.jwt_id); - menu.user = null; //scaMenubar watches for this and re-init - $location.path("/signin"); -}); - -app.controller('SignupController', -function($scope, $route, toaster, $http, $routeParams, scaMessage, $location, $rootScope, jwtHelper) { - $scope.$parent.active_menu = 'signup'; - scaMessage.show(toaster); - $scope.form = {}; - - if($routeParams.jwt) { - $scope.jwt = $routeParams.jwt; +app.controller( + "SuccessController", + function ( + $scope, + $route, + $http, + $routeParams, + $location, + scaMessage, + $sce, + $rootScope + ) { + console.log("successcontroller called"); + scaMessage.success("Welcome back!"); localStorage.setItem($scope.appconf.jwt_id, $routeParams.jwt); - - //register_new sometimes forward here with jwt to finish registration (like setting up email) - var user = jwtHelper.decodeToken($routeParams.jwt); - if(user.profile.username) { - $scope.form.username = user.profile.username; - $scope.username_readonly = true; //if set, user can't change it - } - if(user.profile.email) { - $scope.form.email = user.profile.email; - $scope.email_readonly = true; //if set, user can't change it - } - - //user can change this - because this goes to auth profile - $scope.form.fullname = user.profile.fullname; - } - - $scope.submit = function() { - //new registration (or do registration complete with jwt) - $http.post($scope.appconf.api+'/signup', $scope.form) - .then(function(res, status, headers, config) { - localStorage.setItem($scope.appconf.jwt_id, res.data.jwt); - $rootScope.$broadcast("jwt_update", res.data.jwt); - - //let's post auth profile for the first time - $http.put($scope.appconf.api+'/profile', { - fullname: $scope.form.fullname, - }) - .then(function(_res) { - if(res.data.message) scaMessage.success(res.data.message); - else scaMessage.success("Successfully signed up!"); - - //redirect to somewhere.. - if(res.data.path) $location.path(res.data.path); //maybe .. email_confirmation - else handle_redirect($scope); - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); + $rootScope.$broadcast("jwt_update", $routeParams.jwt); + handle_redirect($scope); } -}); +); + +app.controller( + "SignoutController", + function ( + $scope, + $route, + $http, + $routeParams, + menu, + $location, + scaMessage + ) { + scaMessage.success("Good Bye!"); + localStorage.removeItem($scope.appconf.jwt_id); + menu.user = null; //scaMenubar watches for this and re-init + $location.path("/signin"); + } +); + +app.controller( + "SignupController", + function ( + $scope, + $route, + toaster, + $http, + $routeParams, + scaMessage, + $location, + $rootScope, + jwtHelper + ) { + $scope.$parent.active_menu = "signup"; + scaMessage.show(toaster); + $scope.form = {}; + + if ($routeParams.jwt) { + $scope.jwt = $routeParams.jwt; + localStorage.setItem($scope.appconf.jwt_id, $routeParams.jwt); + + //register_new sometimes forward here with jwt to finish registration (like setting up email) + var user = jwtHelper.decodeToken($routeParams.jwt); + if (user.profile.username) { + $scope.form.username = user.profile.username; + $scope.username_readonly = true; //if set, user can't change it + } + if (user.profile.email) { + $scope.form.email = user.profile.email; + $scope.email_readonly = true; //if set, user can't change it + } -app.controller('AccountController', -function($scope, $route, toaster, $http, jwtHelper, scaMessage) { - $scope.$parent.active_menu = 'user'; - $scope.user = null; - $scope.form_password = {}; - scaMessage.show(toaster); - - var jwt = localStorage.getItem($scope.appconf.jwt_id); - var user = jwtHelper.decodeToken(jwt); - $scope.user = user; - $scope.debug = {jwt: user}; - - $http.get($scope.appconf.api+'/me').then(function(res) { - $scope.user = res.data; - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - - $scope.submit_profile = function() { - $http.put($scope.appconf.api+'/profile', $scope.user) - .then(function(res, status, headers, config) { - $scope.user = res.data; - toaster.success("Profile updated successfully"); - $scope.profile_form.$setPristine(); - }, function(res, status, headers, config) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - } - $scope.submit_password = function() { - $http.put($scope.appconf.api+'/local/setpass', {password_old: $scope.form_password.old, password: $scope.form_password.new}) - .then(function(res, status, headers, config) { - toaster.success(res.data.message); + //user can change this - because this goes to auth profile + $scope.form.fullname = user.profile.fullname; + } - //TODO - why can't put request return updated object? - $http.get($scope.appconf.api+'/me').then(function(res) { - $scope.user = res.data; - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); + $scope.submit = function () { + //new registration (or do registration complete with jwt) + $http.post($scope.appconf.api + "/signup", $scope.form).then( + function (res, status, headers, config) { + localStorage.setItem($scope.appconf.jwt_id, res.data.jwt); + $rootScope.$broadcast("jwt_update", res.data.jwt); + + //let's post auth profile for the first time + $http + .put($scope.appconf.api + "/profile", { + fullname: $scope.form.fullname, + }) + .then( + function (_res) { + if (res.data.message) + scaMessage.success(res.data.message); + else + scaMessage.success( + "Successfully signed up!" + ); + + //redirect to somewhere.. + if (res.data.path) + $location.path(res.data.path); + //maybe .. email_confirmation + else handle_redirect($scope); + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + }; + } +); + +app.controller( + "AccountController", + function ($scope, $route, toaster, $http, jwtHelper, scaMessage) { + $scope.$parent.active_menu = "user"; + $scope.user = null; + $scope.form_password = {}; + scaMessage.show(toaster); + + var jwt = localStorage.getItem($scope.appconf.jwt_id); + var user = jwtHelper.decodeToken(jwt); + $scope.user = user; + $scope.debug = { jwt: user }; + + $http.get($scope.appconf.api + "/me").then( + function (res) { + $scope.user = res.data; + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); else toaster.error(res.statusText); - }); //why do I need to do this? - - //reset the password reset form (mainly to give user visual feedback) - $scope.form_password = {}; - }, function(res, status, headers, config) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - } - - $scope.disconnect = function(type, data) { - $http.put($scope.appconf.api+'/'+type+'/disconnect', data) - .then(function(res) { - toaster.success(res.data.message); - $scope.user = res.data.user; - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - } - - $scope.iucas_connect = function() { - sessionStorage.setItem('auth_redirect', window.location); - var casurl = window.location.origin+window.location.pathname+'iucascb.html'; - window.location = $scope.appconf.iucas_url+'?cassvc=IU&casurl='+casurl; - } - $scope.connect = function(type) { - window.location = "/api/auth/"+type+"/associate/"+jwt; - } - $scope.x509_connect = function() { - window.location = $scope.appconf.x509api+'/x509/associate/'+jwt; - /* + } + ); + + $scope.submit_profile = function () { + $http.put($scope.appconf.api + "/profile", $scope.user).then( + function (res, status, headers, config) { + $scope.user = res.data; + toaster.success("Profile updated successfully"); + $scope.profile_form.$setPristine(); + }, + function (res, status, headers, config) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + }; + $scope.submit_password = function () { + $http + .put($scope.appconf.api + "/local/setpass", { + password_old: $scope.form_password.old, + password: $scope.form_password.new, + }) + .then( + function (res, status, headers, config) { + toaster.success(res.data.message); + + //TODO - why can't put request return updated object? + $http.get($scope.appconf.api + "/me").then( + function (res) { + $scope.user = res.data; + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); //why do I need to do this? + + //reset the password reset form (mainly to give user visual feedback) + $scope.form_password = {}; + }, + function (res, status, headers, config) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + }; + + $scope.disconnect = function (type, data) { + $http + .put($scope.appconf.api + "/" + type + "/disconnect", data) + .then( + function (res) { + toaster.success(res.data.message); + $scope.user = res.data.user; + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + }; + + $scope.iucas_connect = function () { + sessionStorage.setItem("auth_redirect", window.location); + var casurl = + window.location.origin + + window.location.pathname + + "iucascb.html"; + window.location = + $scope.appconf.iucas_url + "?cassvc=IU&casurl=" + casurl; + }; + $scope.connect = function (type) { + window.location = "/api/auth/" + type + "/associate/" + jwt; + }; + $scope.x509_connect = function () { + window.location = $scope.appconf.x509api + "/x509/associate/" + jwt; + /* $http.get($scope.appconf.x509api+'/x509/connect') //, {headers: null}) .then(function(res, status, headers, config) { toaster.success(res.data.message); @@ -268,64 +370,89 @@ function($scope, $route, toaster, $http, jwtHelper, scaMessage) { else toaster.error(res.statusText); }); */ + }; } -}); +); //public interface -app.controller('ForgotpassController', function($scope, $route, toaster, $http, scaMessage, $routeParams, $location) { - scaMessage.show(toaster); - $scope.form = {}; - - if($routeParams.token) { - $scope.state = "reset"; - } else { - $scope.state = "init"; - } - - $scope.submit_email = function() { - $http.post($scope.appconf.api+'/local/resetpass', {email: $scope.form.email}) - .then(function(res) { - toaster.success(res.data.message); - $scope.state = "pending"; - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - } +app.controller( + "ForgotpassController", + function ( + $scope, + $route, + toaster, + $http, + scaMessage, + $routeParams, + $location + ) { + scaMessage.show(toaster); + $scope.form = {}; + + if ($routeParams.token) { + $scope.state = "reset"; + } else { + $scope.state = "init"; + } - $scope.submit_reset = function() { - $http.post($scope.appconf.api+'/local/resetpass', {token: $routeParams.token, password: $scope.form.password}) - .then(function(res) { - scaMessage.success(res.data.message); - $location.path("/"); - }, function(res) { - //if(res.data && res.data.message) toaster.error(res.data.message); - //else toaster.error(res.statusText); - scaMessage.error("Failed to reset password"); - $location.path("/forgotpass"); - }); + $scope.submit_email = function () { + $http + .post($scope.appconf.api + "/local/resetpass", { + email: $scope.form.email, + }) + .then( + function (res) { + toaster.success(res.data.message); + $scope.state = "pending"; + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + }; + + $scope.submit_reset = function () { + $http + .post($scope.appconf.api + "/local/resetpass", { + token: $routeParams.token, + password: $scope.form.password, + }) + .then( + function (res) { + scaMessage.success(res.data.message); + $location.path("/"); + }, + function (res) { + //if(res.data && res.data.message) toaster.error(res.data.message); + //else toaster.error(res.statusText); + scaMessage.error("Failed to reset password"); + $location.path("/forgotpass"); + } + ); + }; } +); -}); - -app.directive('passwordStrength', function() { +app.directive("passwordStrength", function () { return { scope: { password: "=password", - + //optional attributes to make password more secure profile: "=profile", user: "=user", }, - templateUrl: 't/passwordstrength.html', - link: function(scope, element, attributes) { + templateUrl: "t/passwordstrength.html", + link: function (scope, element, attributes) { scope.password_strength = {}; - scope.$watch('password', function(newv, oldv) { - if(newv) { + scope.$watch("password", function (newv, oldv) { + if (newv) { //gather strings that we don't want user to use as password (like user's own fullname, etc..) var used = []; - if(scope.profile) used.push(scope.profile.fullname); - if(scope.user) { + if (scope.profile) used.push(scope.profile.fullname); + if (scope.user) { used.push(scope.user.username); used.push(scope.user.email); } @@ -334,210 +461,320 @@ app.directive('passwordStrength', function() { scope.password_strength = st; } }); - } + }, }; }); -app.controller('AdminUsersController', function($scope, $route, toaster, $http, scaMessage, scaAdminMenu, $location) { - scaMessage.show(toaster); - $scope.$parent.active_menu = 'admin'; - $scope.admin_menu = scaAdminMenu; - - $http.get($scope.appconf.api+'/users') - .then(function(res) { - $scope.users = res.data; - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - $scope.edit = function(id) { - $location.path("/admin/user/"+id); - } -}); - -app.controller('AdminUserController', -function($scope, $route, toaster, $http, scaMessage, scaAdminMenu, $routeParams, $location, $window) { - scaMessage.show(toaster); - $scope.$parent.active_menu = 'admin'; - $scope.admin_menu = scaAdminMenu; - - $http.get($scope.appconf.api+'/user/'+$routeParams.id) - .then(function(res) { - $scope.user = res.data; - if($scope.user.x509dns) $scope.x509dns = JSON.stringify($scope.user.x509dns, null, 4); - if($scope.user.scopes) $scope.scopes = JSON.stringify($scope.user.scopes, null, 4); - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - - $scope.cancel = function() { - $window.history.back(); - } - - $scope.submit = function() { - if($scope.x509dns) $scope.user.x509dns = JSON.parse($scope.x509dns); - $scope.user.scopes = JSON.parse($scope.scopes); - - $http.put($scope.appconf.api+'/user/'+$routeParams.id, $scope.user) - .then(function(res) { - $location.path("/admin/users"); - toaster.success(res.data.message); - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - } -}); - -app.controller('GroupsController', function($scope, $route, toaster, $http, scaMessage, profiles, $location) { - $scope.$parent.active_menu = 'groups'; - scaMessage.show(toaster); - - profiles.then(function(_users) { - $scope.users = _users; - $http.get($scope.appconf.api+'/groups') - .then(function(res) { - $scope.groups = res.data; - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); +app.controller( + "AdminUsersController", + function ( + $scope, + $route, + toaster, + $http, + scaMessage, + scaAdminMenu, + $location + ) { + scaMessage.show(toaster); + $scope.$parent.active_menu = "admin"; + $scope.admin_menu = scaAdminMenu; + + $http.get($scope.appconf.api + "/users").then( + function (res) { + $scope.users = res.data; + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + $scope.edit = function (id) { + $location.path("/admin/user/" + id); + }; + } +); + +app.controller( + "AdminUserController", + function ( + $scope, + $route, + toaster, + $http, + scaMessage, + scaAdminMenu, + $routeParams, + $location, + $window + ) { + scaMessage.show(toaster); + $scope.$parent.active_menu = "admin"; + $scope.admin_menu = scaAdminMenu; + + $http.get($scope.appconf.api + "/user/" + $routeParams.id).then( + function (res) { + $scope.user = res.data; + if ($scope.user.x509dns) + $scope.x509dns = JSON.stringify( + $scope.user.x509dns, + null, + 4 + ); + if ($scope.user.scopes) + $scope.scopes = JSON.stringify($scope.user.scopes, null, 4); + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + + $scope.cancel = function () { + $window.history.back(); + }; + + $scope.submit = function () { + if ($scope.x509dns) + $scope.user.x509dns = JSON.parse($scope.x509dns); + $scope.user.scopes = JSON.parse($scope.scopes); + + $http + .put( + $scope.appconf.api + "/user/" + $routeParams.id, + $scope.user + ) + .then( + function (res) { + $location.path("/admin/users"); + toaster.success(res.data.message); + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + }; + } +); + +app.controller( + "GroupsController", + function ($scope, $route, toaster, $http, scaMessage, profiles, $location) { + $scope.$parent.active_menu = "groups"; + scaMessage.show(toaster); + + profiles.then(function (_users) { + $scope.users = _users; + $http.get($scope.appconf.api + "/groups").then( + function (res) { + $scope.groups = res.data; + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); }); - }); - $scope.edit = function(id) { - $location.path("/group/"+id); - } -}); - -app.controller('GroupController', function($scope, $route, toaster, $http, jwtHelper, scaMessage, $routeParams, $location, profiles) { - scaMessage.show(toaster); - $scope.$parent.active_menu = 'groups'; - var jwt = localStorage.getItem($scope.appconf.jwt_id); - var user = jwtHelper.decodeToken(jwt); - $scope.group = { - active: true, - }; - $scope.admins = []; - $scope.members = []; - - profiles.then(function(_users) { - $scope.users = _users; - if($routeParams.id != 'new') { - load_group($routeParams.id); - } else { - //add the user as first admin - _users.forEach(function(_user) { - if(_user.id == user.sub) { - $scope.admins.push(_user); - } - }); - } - }); - - function load_group(id) { - $http.get($scope.appconf.api+'/group/'+id) - .then(function(res) { - $scope.group = res.data; - - $scope.admins = []; - $scope.group.Admins.forEach(function(admin) { - $scope.users.forEach(function(user) { - if(admin.id == user.id) $scope.admins.push(user); - }); - }); - - $scope.members = []; - $scope.group.Members.forEach(function(member) { - $scope.users.forEach(function(user) { - if(member.id == user.id) $scope.members.push(user); + $scope.edit = function (id) { + $location.path("/group/" + id); + }; + } +); + +app.controller( + "GroupController", + function ( + $scope, + $route, + toaster, + $http, + jwtHelper, + scaMessage, + $routeParams, + $location, + profiles + ) { + scaMessage.show(toaster); + $scope.$parent.active_menu = "groups"; + var jwt = localStorage.getItem($scope.appconf.jwt_id); + var user = jwtHelper.decodeToken(jwt); + $scope.group = { + active: true, + }; + $scope.admins = []; + $scope.members = []; + + profiles.then(function (_users) { + $scope.users = _users; + if ($routeParams.id != "new") { + load_group($routeParams.id); + } else { + //add the user as first admin + _users.forEach(function (_user) { + if (_user.id == user.sub) { + $scope.admins.push(_user); + } }); - }); - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - } - - $scope.submit = function() { - var admins = []; - $scope.admins.forEach(function(admin) { - admins.push(admin.id); - }); - var members = []; - $scope.members.forEach(function(member) { - members.push(member.id); + } }); - var body = { - group: $scope.group, - admins: admins, - members: members, - } - //ui-select require doesn't work so I need to have this - if(admins.length == 0) { - toaster.error("Please specify at least 1 admin"); - return; + function load_group(id) { + $http.get($scope.appconf.api + "/group/" + id).then( + function (res) { + $scope.group = res.data; + + $scope.admins = []; + $scope.group.Admins.forEach(function (admin) { + $scope.users.forEach(function (user) { + if (admin.id == user.id) $scope.admins.push(user); + }); + }); + + $scope.members = []; + $scope.group.Members.forEach(function (member) { + $scope.users.forEach(function (user) { + if (member.id == user.id) $scope.members.push(user); + }); + }); + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); } - if($routeParams.id == "new") { - //new - $http.post($scope.appconf.api+'/group', body) - .then(function(res) { - $location.path("/groups"); - toaster.success(res.data.message); - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); + $scope.submit = function () { + var admins = []; + $scope.admins.forEach(function (admin) { + admins.push(admin.id); }); - } else { - //update - $http.put($scope.appconf.api+'/group/'+$routeParams.id, body) - .then(function(res) { - $location.path("/groups"); - toaster.success(res.data.message); - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); + var members = []; + $scope.members.forEach(function (member) { + members.push(member.id); }); - } - } - $scope.cancel = function() { - $location.path("/groups"); - } -}); - -app.controller('SendEmailConfirmationController', function($scope, $route, toaster, $http, scaMessage, scaAdminMenu, $routeParams, $location) { - scaMessage.show(toaster); - $scope.$parent.active_menu = 'user'; -}); - -app.controller('ConfirmEmailController', function($scope, $route, toaster, $http, scaMessage, scaAdminMenu, $routeParams, $location) { - scaMessage.show(toaster); - $scope.$parent.active_menu = 'user'; - $scope.sub = $routeParams.sub; - - $scope.resend = function() { - $http.post($scope.appconf.api+'/send_email_confirmation', {sub: $scope.sub}) - .then(function(res) { - toaster.success(res.data.message); - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); - } + var body = { + group: $scope.group, + admins: admins, + members: members, + }; + + //ui-select require doesn't work so I need to have this + if (admins.length == 0) { + toaster.error("Please specify at least 1 admin"); + return; + } - //this page also handles the actual confirmation request back from the email - if($routeParams.token) { - $http.post($scope.appconf.api+'/confirm_email', {token: $routeParams.token}) - .then(function(res) { - console.log("email confirmation successfull"); - scaMessage.success(res.data.message); - $location.path("/"); - }, function(res) { - if(res.data && res.data.message) toaster.error(res.data.message); - else toaster.error(res.statusText); - }); + if ($routeParams.id == "new") { + //new + $http.post($scope.appconf.api + "/group", body).then( + function (res) { + $location.path("/groups"); + toaster.success(res.data.message); + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + } else { + //update + $http + .put($scope.appconf.api + "/group/" + $routeParams.id, body) + .then( + function (res) { + $location.path("/groups"); + toaster.success(res.data.message); + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + } + }; + $scope.cancel = function () { + $location.path("/groups"); + }; + } +); + +app.controller( + "SendEmailConfirmationController", + function ( + $scope, + $route, + toaster, + $http, + scaMessage, + scaAdminMenu, + $routeParams, + $location + ) { + scaMessage.show(toaster); + $scope.$parent.active_menu = "user"; + } +); + +app.controller( + "ConfirmEmailController", + function ( + $scope, + $route, + toaster, + $http, + scaMessage, + scaAdminMenu, + $routeParams, + $location + ) { + scaMessage.show(toaster); + $scope.$parent.active_menu = "user"; + $scope.sub = $routeParams.sub; + + $scope.resend = function () { + $http + .post($scope.appconf.api + "/send_email_confirmation", { + sub: $scope.sub, + }) + .then( + function (res) { + toaster.success(res.data.message); + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + }; + + //this page also handles the actual confirmation request back from the email + if ($routeParams.token) { + $http + .post($scope.appconf.api + "/confirm_email", { + token: $routeParams.token, + }) + .then( + function (res) { + console.log("email confirmation successfull"); + scaMessage.success(res.data.message); + $location.path("/"); + }, + function (res) { + if (res.data && res.data.message) + toaster.error(res.data.message); + else toaster.error(res.statusText); + } + ); + } } -}); - - +); diff --git a/ui/package.json b/ui/package.json index f2d967d..f704e72 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,31 +1,31 @@ { - "name": "sca-auth-ui", - "version": "4.3.2", - "description": "", - "main": "common.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "Soichi Hayashi (http://soichi.us)", - "license": "ISC", - "dependencies": { - "angular": "^1.6.4", - "angular-animate": "^1.6.4", - "angular-bootstrap": "^0.12.2", - "angular-cookies": "^1.6.4", - "angular-gravatar": "^0.4.2", - "angular-jwt": "^0.1.8", - "angular-loading-bar": "^0.9.0", - "angular-route": "^1.6.4", - "angular-sanitize": "^1.6.4", - "angular-ui-select": "^0.12.100", - "angularjs-toaster": "^2.1.0", - "bootstrap": "^3.3.7", - "bootswatch": "^3.3.7", - "font-awesome": "^4.7.0", - "jquery": "^3.2.1", - "sca-shared": "0.0.9", - "ui-select": "^0.19.6", - "zxcvbn": "^4.4.2" - } + "name": "sca-auth-ui", + "version": "4.3.2", + "description": "", + "main": "common.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Soichi Hayashi (http://soichi.us)", + "license": "ISC", + "dependencies": { + "angular": "^1.6.4", + "angular-animate": "^1.6.4", + "angular-bootstrap": "^0.12.2", + "angular-cookies": "^1.6.4", + "angular-gravatar": "^0.4.2", + "angular-jwt": "^0.1.8", + "angular-loading-bar": "^0.9.0", + "angular-route": "^1.6.4", + "angular-sanitize": "^1.6.4", + "angular-ui-select": "^0.12.100", + "angularjs-toaster": "^2.1.0", + "bootstrap": "^3.3.7", + "bootswatch": "^3.3.7", + "font-awesome": "^4.7.0", + "jquery": "^3.2.1", + "sca-shared": "0.0.9", + "ui-select": "^0.19.6", + "zxcvbn": "^4.4.2" + } } diff --git a/ui/t/account.html b/ui/t/account.html index 11d4f6d..e92f4d5 100644 --- a/ui/t/account.html +++ b/ui/t/account.html @@ -1,34 +1,63 @@
-

Settings

+

Settings

- +
-
+
  • Account Profile

  • -

    You will need to sign out / login again before your change to account profile takes effect.

    +

    + You will need to sign out / login again before your change + to account profile takes effect. +

    - +
    -

    An email address used to login to your account

    +

    + An email address used to login to your account +

    - +
    - +
    - +
  • @@ -36,24 +65,53 @@

    Change Password

  • -

    You have not set your password for this application.

    +

    + You have not set your password for this application. +

    - I forgot my password + I forgot my password - +
    - + - +
    - +
    - -
    + + +
  • Connected Accounts

    @@ -61,101 +119,251 @@

    Connected Accounts

  • - -

    - {{user.username}} | - Last Login: + +

    + {{user.username}} | + Last Login: Never - +

    LDAP

    -
    +
- - - -

+ + + +

{{user.iucas}} | - Last Login: + Last Login: Never - +

Indiana University CAS

-
+
- - -

- {{user.googleid}} | - Last Login: + + +

+ {{user.googleid}} | + Last Login: Never - +

-

Google

+

+ Google +

- - -

- {{user.github}} | - Last Login: + + +

+ {{user.github}} | + Last Login: Never - +

-

Github

+

+ Github +

- - - -

+ + + +

{{user.orcid}} | - Last Login: + Last Login: Never - +

ORCID

- - -

- {{user.facebook}} | - Last Login: + + +

+ {{user.facebook}} | + Last Login: Never - +

-

Facebook

+

+ + Facebook +

- - + +

X509 Certificate

-
    -
  • - +
      +
    • +

      - {{dn}} - | Last Login: - Never - + {{dn}} + + | Last Login: + Never +

- - + +

{{appconf.oidc_label||'OpenID Connect'}}

-
    -
  • - +
      +
    • +

      - {{profile.idp_name}} {{profile.email}} - | Last Login: - Never - + {{profile.idp_name}} + {{profile.email}} + + | Last Login: + Never +

    • @@ -163,7 +371,10 @@

      {{appconf.oidc_label||'OpenID Connect'}}

-
  • +
  • Nerdy Things

  • @@ -173,5 +384,6 @@

    User Record

    {{user|json}}
  • - + +
    diff --git a/ui/t/adminuser.html b/ui/t/adminuser.html index 1dfb0aa..36a2cc5 100644 --- a/ui/t/adminuser.html +++ b/ui/t/adminuser.html @@ -4,70 +4,106 @@

    Settings

    - + -
    +

    This is a prototype

    {{user.username}}

    -

    TODO.. I will update this to a bit more user friendly esitor

    - +

    + TODO.. I will update this to a bit more user friendly esitor +

    +
    -
    +
    - +
    - +
    - +
    -
    +
    - +
    - +
    - +
    -

    TODO.. I will update this to a bit more user friendly esitor

    - +

    + TODO.. I will update this to a bit more user friendly esitor +

    +
    -
    +
    - +
    -
    +
    -
    +
    {{user.times|json}}
    - + +
    - diff --git a/ui/t/adminusers.html b/ui/t/adminusers.html index 77d74f4..ca50a58 100644 --- a/ui/t/adminusers.html +++ b/ui/t/adminusers.html @@ -4,28 +4,53 @@

    Settings

    - + -
    +

    This is a prototype

    • - +
      -

      {{user.id}} {{user.username}} - Inactive +

      + {{user.id}} {{user.username}} + Inactive

      {{user.fullname}} | - {{user.email}} Confirmed | + + {{user.email}} + Confirmed + | + X509DNs: {{user.x509dns}} | IUCAS: {{user.iucas}} | - Google ID: {{user.googleid}} | + Google ID: {{user.googleid}} | + Git ID: {{user.gitid}} | - password set + password set

      - +
      Scopes @@ -35,9 +60,10 @@

      {{user.id}} {{user.username}} Timestamps
      {{user.times|json}}

      -
      +
    + - + +
    - diff --git a/ui/t/confirm_email.html b/ui/t/confirm_email.html index cfc4b82..a3fa055 100644 --- a/ui/t/confirm_email.html +++ b/ui/t/confirm_email.html @@ -1,13 +1,18 @@

    Email Confirmation

    -

    Please check your email inbox and follow the instruction found in the email.

    -
    +

    + Please check your email inbox and follow the instruction found in + the email. +

    +

    Or ... - +

    - + +
    - diff --git a/ui/t/footer.html b/ui/t/footer.html index b0b31f3..f8fc74d 100644 --- a/ui/t/footer.html +++ b/ui/t/footer.html @@ -1,11 +1,13 @@ -
    +
    -
    +
    -

    Indiana University | - +

    + Indiana University | +

    -
    +
    - diff --git a/ui/t/forgotpass.html b/ui/t/forgotpass.html index 49e318e..fb68c61 100644 --- a/ui/t/forgotpass.html +++ b/ui/t/forgotpass.html @@ -4,34 +4,62 @@

    Reset Password

    -

    Please enter an email address to send password reset instruction.

    - +

    + Please enter an email address to send password reset + instruction. +

    +
    - +
    -

    Password reset instruction email has been sent. Please follow the instruction on your email.

    +

    + Password reset instruction email has been sent. Please follow the + instruction on your email. +

    Please enter new password

    - - + +
    - +
    - +
    -
    -
    -

    Please contact the administrator for assistance.

    +
    +
    +

    + Please contact the + administrator for + assistance. +

    - + + - diff --git a/ui/t/group.html b/ui/t/group.html index e59d9f1..164adb8 100644 --- a/ui/t/group.html +++ b/ui/t/group.html @@ -4,15 +4,26 @@

    Settings

    - + -
    -

    User needs to refresh login token for group settings to take effect.

    +
    +

    + User needs to refresh login token for group settings to take effect. +

    - +
    @@ -25,13 +36,19 @@

    Settings

    - - - {{$item.fullname}} + + + {{$item.fullname}} + - - - {{user.fullname}} <{{user.email}}> + + + {{user.fullname}} + <{{user.email}}>
    @@ -41,12 +58,16 @@

    Settings

    - - {{$item.fullname}} + + {{$item.fullname}} + - - - {{user.fullname}} <{{user.email}}> + + + {{user.fullname}} + <{{user.email}}>
    @@ -55,17 +76,22 @@

    Settings

    - - + +
    -
    - +
    + +
    - diff --git a/ui/t/groups.html b/ui/t/groups.html index 24c7824..e6d4481 100644 --- a/ui/t/groups.html +++ b/ui/t/groups.html @@ -4,20 +4,34 @@

    Settings

    - + -
    - -
    -
    +
    + +
    +
    • - +

      - {{group.id}} {{group.name}} - Inactive + {{group.id}} {{group.name}} + Inactive

      {{group.desc}}

      - +
      Admins
      @@ -46,9 +60,10 @@
      Members
      {{group.Members|json}}
      -->
      -
      +
    + - + +
    - diff --git a/ui/t/inactive.html b/ui/t/inactive.html index df6a05a..52e4442 100644 --- a/ui/t/inactive.html +++ b/ui/t/inactive.html @@ -1,9 +1,15 @@
    -

    Account Disabled

    +

    + Account Disabled +

    Your account is currently disabled.

    -

    Please contact the administrator for more detail.

    +

    + Please contact the + administrator for more + detail. +

    - + +
    - diff --git a/ui/t/passwordstrength.html b/ui/t/passwordstrength.html index 1f1a3c8..25aa18a 100644 --- a/ui/t/passwordstrength.html +++ b/ui/t/passwordstrength.html @@ -1,9 +1,19 @@
    -Very Weak {{password_strength.feedback.suggestions}} -Weak {{password_strength.feedback.suggestions}} -OK -Strong -Very Strong + Very Weak {{password_strength.feedback.suggestions}} + Weak {{password_strength.feedback.suggestions}} + OK + Strong + Very Strong
    diff --git a/ui/t/signin.html b/ui/t/signin.html index a55da52..6a996d1 100644 --- a/ui/t/signin.html +++ b/ui/t/signin.html @@ -1,97 +1,186 @@
    - +
    -
    -
    -
    -
    -
    -
    -
    - - + + Redirecting to {{$select.selected.org}} - + {{idp.org}} {{idp.home}}
    -
    +
    or
    -
    - - +
    + +
    - - + + - +
    - - + +
    - +
    -
    +
    -
    First time here?   Sign Up
    -
    +
    + First time here?   Sign Up +
    +
    Forgot Password? @@ -103,4 +192,3 @@ -->
    - diff --git a/ui/t/signup.html b/ui/t/signup.html index 14714f2..2996edd 100644 --- a/ui/t/signup.html +++ b/ui/t/signup.html @@ -2,37 +2,76 @@

    Signup

    -

    This is your first time you've accessed this application. Please complete your registration.

    +

    + This is your first time you've accessed this application. Please + complete your registration. +

    - +
    - +
    - +
    - - + +
    - +
    - +
    - + +
    diff --git a/ui/t/userlist.html b/ui/t/userlist.html index 3da1a55..af129b0 100644 --- a/ui/t/userlist.html +++ b/ui/t/userlist.html @@ -1,8 +1,8 @@

      No users specified

    • - - {{user.fullname}} + + {{user.fullname}} +
    -